Completed
Push — 16.1 ( af7579...96a09f )
by Nathan
19:02
created

app.js ➔ ... ➔ AppJS.extend.custom_mail   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 1
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
/**
2
 * EGroupware - Calendar - Javascript UI
3
 *
4
 * @link http://www.egroupware.org
5
 * @package calendar
6
 * @author Hadi Nategh	<hn-AT-stylite.de>
7
 * @author Nathan Gray
8
 * @copyright (c) 2008-16 by Ralf Becker <RalfBecker-AT-outdoor-training.de>
9
 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
10
 * @version $Id$
11
 */
12
13
/*egw:uses
14
	/etemplate/js/etemplate2.js;
15
	/calendar/js/et2_widget_owner.js;
16
	/calendar/js/et2_widget_timegrid.js;
17
	/calendar/js/et2_widget_planner.js;
18
	/vendor/bower-asset/jquery-touchswipe/jquery.touchSwipe.js;
19
*/
20
21
/**
22
 * UI for calendar
23
 *
24
 * Calendar has multiple different views of the same data.  All the templates
25
 * for the different view are loaded at the start, then the view objects
26
 * in app.classes.calendar.views are used to manage the different views.
27
 * update_state() is used to change the state between the different views, as
28
 * well as adjust only a single part of the state while keeping the rest unchanged.
29
 *
30
 * The event widgets (and the nextmatch) get the data from egw.data, and they
31
 * register update callbacks to automatically update when the data changes.  This
32
 * means that when we update something on the server, to update the UI we just need
33
 * to send back the new data and if the event widget still exists it will update
34
 * itself.  See calendar_uiforms->ajax_status().
35
 *
36
 * To reduce server calls, we also keep a map of day => event IDs.  This allows
37
 * us to quickly change views (week to day, for example) without requesting additional
38
 * data from the server.  We keep that map as long as only the date (and a few
39
 * others - see update_state()) changes.  If user or any of the other filters are
40
 * changed, we discard the daywise cache and ask the server for the filtered events.
41
 *
42
 * @augments AppJS
43
 */
44
app.classes.calendar = (function(){ "use strict"; return AppJS.extend(
45
{
46
	/**
47
	 * application name
48
	 */
49
	appname: 'calendar',
50
51
	/**
52
	 * etemplate for the sidebox filters
53
	 */
54
	sidebox_et2: null,
55
56
	/**
57
	 * Current internal state
58
	 *
59
	 * If you need to change state, you can pass just the fields to change to
60
	 * update_state().
61
	 */
62
	state: {
63
		date: new Date(),
64
		view: egw.preference('saved_states','calendar') ? egw.preference('saved_states','calendar').view : egw.preference('defaultcalendar','calendar') || 'day',
65
		owner: egw.user('account_id')
66
	},
67
68
	/**
69
	 * These are the keys we keep to set & remember the status, others are discarded
70
	 */
71
	states_to_save: ['owner','status_filter','filter','cat_id','view','sortby','planner_view','weekend'],
72
73
	// If you are in one of these views and select a date in the sidebox, the view
74
	// will change as needed to show the date.  Other views will only change the
75
	// date in the current view.
76
	sidebox_changes_views: ['day','week','month'],
77
78
	// Calendar allows other apps to hook into the sidebox.  We keep these etemplates
79
	// up to date as state is changed.
80
	sidebox_hooked_templates: [],
81
82
	// List of queries in progress, to prevent home from requesting the same thing
83
	_queries_in_progress: [],
84
85
	// Calendar-wide autorefresh
86
	_autorefresh_timer: null,
87
88
	/**
89
	 * Constructor
90
	 *
91
	 * @memberOf app.calendar
92
	 */
93
	init: function()
94
	{
95
		// categories have nothing to do with calendar, but eT2 objects loads calendars app.js
96
		if (window.framework && framework.applications.calendar.browser &&
97
			framework.applications.calendar.browser.currentLocation.match('menuaction=preferences\.preferences_categories_ui\.index'))
98
		{
99
			this._super.apply(this, arguments);
100
			return;
101
		}
102
		else// make calendar object available, even if not running in top window, as sidebox does
103
		if (window.top !== window && !egw(window).is_popup() && window.top.app.calendar)
104
		{
105
			window.app.calendar = window.top.app.calendar;
106
			return;
107
		}
108
		else if (window.top == window && !egw(window).is_popup())
109
		{
110
			// Show loading div
111
			egw.loading_prompt(
112
				this.appname,true,egw.lang('please wait...'),
113
				typeof framework !== 'undefined' ? framework.applications.calendar.tab.contentDiv : false,
114
				egwIsMobile()?'horizental':'spinner'
115
			);
116
		}
117
118
		// call parent
119
		this._super.apply(this, arguments);
120
121
		// Scroll
122
		jQuery(jQuery.proxy(this._scroll,this));
123
		jQuery.extend(this.state, this.egw.preference('saved_states','calendar'));
124
125
		// Set custom color for events without category
126
		if(this.egw.preference('no_category_custom_color','calendar'))
127
		{
128
			this.egw.css(
129
				'.calendar_calEvent:not([class*="cat_"])',
130
				'background-color: '+this.egw.preference('no_category_custom_color','calendar')+' !important'
131
			);
132
		}
133
	},
134
135
	/**
136
	 * Destructor
137
	 */
138
	destroy: function()
139
	{
140
		// call parent
141
		this._super.apply(this, arguments);
142
143
		// remove top window reference
144
		if (window.top !== window && window.top.app.calendar === this)
145
		{
146
			delete window.top.app.calendar;
147
		}
148
		jQuery('body').off('.calendar');
149
150
		if(this.sidebox_et2)
151
		{
152
			var date = this.sidebox_et2.getWidgetById('date');
153
			jQuery(window).off('resize.calendar'+date.dom_id);
154
		}
155
		this.sidebox_hooked_templates = null;
156
157
		egw_unregisterGlobalShortcut(jQuery.ui.keyCode.PAGE_UP, false, false, false);
158
		egw_unregisterGlobalShortcut(jQuery.ui.keyCode.PAGE_DOWN, false, false, false);
159
160
		// Stop autorefresh
161
		if(this._autorefresh_timer)
162
		{
163
			window.clearInterval(this._autorefresh_timer);
164
			this._autorefresh_timer = null;
165
		}
166
	},
167
168
	/**
169
	 * This function is called when the etemplate2 object is loaded
170
	 * and ready.  If you must store a reference to the et2 object,
171
	 * make sure to clean it up in destroy().
172
	 *
173
	 * @param {etemplate2} _et2 newly ready et2 object
174
	 * @param {string} _name name of template
175
	 */
176
	et2_ready: function(_et2, _name)
177
	{
178
		// call parent
179
		this._super.apply(this, arguments);
180
181
		// Avoid many problems with home
182
		if(_et2.app !== 'calendar' || _name == 'admin.categories.index')
183
		{
184
			egw.loading_prompt(this.appname,false);
185
			return;
186
		}
187
188
		// Re-init sidebox, since it was probably initialized too soon
189
		var sidebox = jQuery('#favorite_sidebox_'+this.appname);
190
		if(sidebox.length == 0 && egw_getFramework() != null)
191
		{
192
			var egw_fw = egw_getFramework();
193
			sidebox= jQuery('#favorite_sidebox_'+this.appname,egw_fw.sidemenuDiv);
194
		}
195
196
		var content = this.et2.getArrayMgr('content');
197
198
		switch (_name)
0 ignored issues
show
Coding Style introduced by
As per coding-style, switch statements should have a default case.
Loading history...
199
		{
200
			case 'calendar.sidebox':
201
				this.sidebox_et2 = _et2.widgetContainer;
202
				this.sidebox_hooked_templates.push(this.sidebox_et2);
203
				jQuery(_et2.DOMContainer).hide();
204
205
				// Set client side holiday cache for this year
206
				if(egw.window.et2_calendar_view)
207
				{
208
					egw.window.et2_calendar_view.holiday_cache[content.data.year] = content.data.holidays;
209
					delete content.data.holidays;
210
					delete content.data.year;
211
				}
212
213
				this._setup_sidebox_filters();
214
215
				this.state = content.data;
216
				break;
217
218
			case 'calendar.edit':
219
				if (typeof content.data['conflicts'] == 'undefined')
220
				{
221
					//Check if it's fallback from conflict window or it's from edit window
222
					if (content.data['button_was'] != 'freetime')
223
					{
224
						this.set_enddate_visibility();
225
						this.check_recur_type();
226
						this.edit_start_change();
227
						this.et2.getWidgetById('recur_exception').set_disabled(!content.data.recur_exception ||
228
							typeof content.data.recur_exception[0] == 'undefined');
229
					}
230
					else
231
					{
232
						this.freetime_search();
233
					}
234
					//send Syncronus ajax request to the server to unlock the on close entry
235
					//set onbeforeunload with json request to send request when the window gets close by X button
236
					if (content.data.lock_token)
237
					{
238
						window.onbeforeunload = function () {
239
							this.egw.json('calendar.calendar_uiforms.ajax_unlock',
240
							[content.data.id, content.data.lock_token],null,true,null,null).sendRequest(true);
241
						};
242
					}
243
				}
244
				this.alarm_custom_date();
245
246
				// If title is pre-filled for a new (no ID) event, highlight it
247
				if(content.data && !content.data.id && content.data.title)
248
				{
249
					this.et2.getWidgetById('title').input.select();
250
				}
251
252
				// Disable loading prompt (if loaded nopopup)
253
				egw.loading_prompt(this.appname,false);
254
				break;
255
256
			case 'calendar.freetimesearch':
257
				this.set_enddate_visibility();
258
				break;
259
			case 'calendar.list':
260
				// Wait until _et2_view_init is done
261
				window.setTimeout(jQuery.proxy(function() {
262
					this.filter_change();
263
				},this),0);
264
				break;
265
			case 'calendar.category_report':
266
				this.category_report_init();
267
				break;
268
		}
269
270
		// Record the templates for the views so we can switch between them
271
		this._et2_view_init(_et2,_name);
272
	},
273
274
	/**
275
	 * Observer method receives update notifications from all applications
276
	 *
277
	 * App is responsible for only reacting to "messages" it is interested in!
278
	 *
279
	 * Calendar binds listeners to the data cache, so if the data is updated, the widget
280
	 * will automatically update itself.
281
	 *
282
	 * @param {string} _msg message (already translated) to show, eg. 'Entry deleted'
283
	 * @param {string} _app application name
284
	 * @param {(string|number)} _id id of entry to refresh or null
285
	 * @param {string} _type either 'update', 'edit', 'delete', 'add' or null
286
	 * - update: request just modified data from given rows.  Sorting is not considered,
287
	 *		so if the sort field is changed, the row will not be moved.
288
	 * - edit: rows changed, but sorting may be affected.  Requires full reload.
289
	 * - delete: just delete the given rows clientside (no server interaction neccessary)
290
	 * - add: requires full reload for proper sorting
291
	 * @param {string} _msg_type 'error', 'warning' or 'success' (default)
292
	 * @param {object|null} _links app => array of ids of linked entries
293
	 * or null, if not triggered on server-side, which adds that info
294
	 * @return {false|*} false to stop regular refresh, thought all observers are run
295
	 */
296
	observer: function(_msg, _app, _id, _type, _msg_type, _links)
297
	{
298
		var do_refresh = false;
299
		if(this.state.view === 'listview')
300
		{
301
			app.classes.calendar.views.listview.etemplates[0].widgetContainer.getWidgetById('nm').refresh(_id,_type);
302
		}
303
		switch(_app)
0 ignored issues
show
Coding Style introduced by
As per coding-style, switch statements should have a default case.
Loading history...
304
		{
305
			case 'infolog':
306
				jQuery('.calendar_calDayTodos')
307
					.find('a')
308
					.each(function(i,a){
309
						var match = a.href.split(/&info_id=/);
310
						if (match && typeof match[1] !="undefined")
311
						{
312
							if (match[1]== _id)	do_refresh = true;
313
						}
314
					});
315
316
				// Unfortunately we do not know what type this infolog is here,
317
				// but we can tell if it's turned off entirely
318
				if(egw.preference('calendar_integration','infolog') !== '0')
319
				{
320
					if (jQuery('div [data-app="infolog"][data-app_id="'+_id+'"]').length > 0) do_refresh = true;
321
					switch (_type)
322
					{
323
						case 'add':
324
							do_refresh = true;
325
							break;
326
					}
327
				}
328
				if (do_refresh)
329
				{
330
					// Discard cache
331
					this._clear_cache();
332
333
					// Calendar is the current application, refresh now
334
					if(framework.activeApp.appName === this.appname)
335
					{
336
						this.setState({state: this.state});
337
					}
338
					// Bind once to trigger a refresh when tab is activated again
339
					else if(framework.applications.calendar && framework.applications.calendar.tab &&
340
						framework.applications.calendar.tab.contentDiv)
341
					{
342
						jQuery(framework.applications.calendar.tab.contentDiv)
343
							.off('show.calendar')
344
							.one('show.calendar',
345
								jQuery.proxy(function() {this.setState({state: this.state});},this)
346
							);
347
					}
348
				}
349
				break;
350
			case 'calendar':
351
				// Regular refresh
352
				var event = false;
353
				if(_id)
354
				{
355
					event = egw.dataGetUIDdata('calendar::'+_id);
356
				}
357
				if(event && event.data && event.data.date || _type === 'delete')
358
				{
359
					// Intelligent refresh without reloading everything
360
					var recurrences = Object.keys(egw.dataSearchUIDs(new RegExp('^calendar::'+_id+':')));
361
					var ids = event && event.data && event.data.recur_type && typeof _id === 'string' && _id.indexOf(':') < 0 || recurrences.length ?
362
						recurrences :
363
						['calendar::'+_id];
364
365
					if(_type === 'delete')
366
					{
367
						for(var i in ids)
368
						{
369
							egw.dataStoreUID(ids[i], null);
370
						}
371
					}
372
					// Updates are handled by events themselves through egw.data
373
					else if (_type !== 'update')
374
					{
375
						this._update_events(this.state, ids);
376
					}
377
					return false;
378
				}
379
				else
380
				{
381
					this._clear_cache();
382
383
					// Force redraw to current state
384
					this.setState({state: this.state});
385
					return false;
386
				}
387
				break;
0 ignored issues
show
Unused Code introduced by
This break statement is unnecessary and may be removed.
Loading history...
388
		}
0 ignored issues
show
Comprehensibility introduced by
There is no default case in this switch, so nothing gets returned when all cases fail. You might want to consider adding a default or return undefined explicitly.
Loading history...
389
	},
390
391
	/**
392
	 * Link hander for jDots template to just reload our iframe, instead of reloading whole admin app
393
	 *
394
	 * @param {String} _url
395
	 * @return {boolean|string} true, if linkHandler took care of link, false for default processing or url to navigate to
396
	 */
397
	linkHandler: function(_url)
398
	{
399
		if (_url == 'about:blank' || _url.match('menuaction=preferences\.preferences_categories_ui\.index'))
400
		{
401
			return false;
402
		}
403
		if (_url.match('menuaction=calendar\.calendar_uiviews\.'))
404
		{
405
			var view = _url.match(/calendar_uiviews\.([^&?]+)/);
406
			view = view && view.length > 1 ? view[1] : null;
407
408
			// Get query
409
			var q = {};
410
			_url.split('?')[1].split('&').forEach(function(i){
411
				q[i.split('=')[0]]=unescape(i.split('=')[1]);
412
			});
413
			delete q.ajax;
414
			delete q.menuaction;
415
			if(!view && q.view || q.view != view && view == 'index') view = q.view;
416
417
			// No specific view requested, looks like a reload from framework
418
			if(this.sidebox_et2 && typeof view === 'undefined')
419
			{
420
				this._clear_cache();
421
				this.setState({state: this.state});
422
				return false;
423
			}
424
425
			if (this.sidebox_et2 && typeof app.classes.calendar.views[view] == 'undefined' && view != 'index')
426
			{
427
				if(q.owner)
428
				{
429
					q.owner = q.owner.split(',');
430
					q.owner = q.owner.reduce(function(p,c) {if(p.indexOf(c)<0) p.push(c);return p;},[]);
431
					q.owner = q.owner.join(',');
432
				}
433
				q.menuaction = 'calendar.calendar_uiviews.index';
434
				this.sidebox_et2.getWidgetById('iframe').set_src(egw.link('/index.php',q));
435
				jQuery(this.sidebox_et2.parentNode).show();
436
				return true;
437
			}
438
			// Known AJAX view
439
			else if(app.classes.calendar.views[view])
440
			{
441
				// Reload of known view?
442
				if(view == 'index')
443
				{
444
					var pref = this.egw.preference('saved_states','calendar');
445
					view = pref.view || 'day';
446
				}
447
				// View etemplate not loaded
448
				if(typeof app.classes.calendar.views[view].etemplates[0] == 'string')
449
				{
450
					return _url + '&ajax=true';
451
				}
452
				// Already loaded, we'll just apply any variables to our current state
453
				var set = jQuery.extend({view: view},q);
454
				this.update_state(set);
455
				return true;
456
			}
457
		}
458
		else if (this.sidebox_et2)
459
		{
460
			var iframe = this.sidebox_et2.getWidgetById('iframe');
461
			if(!iframe) return false;
462
			iframe.set_src(_url);
463
			jQuery(this.sidebox_et2.parentNode).show();
464
			// Hide other views
465
			for(var _view in app.classes.calendar.views)
466
			{
467
				for(var i = 0; i < app.classes.calendar.views[_view].etemplates.length; i++)
468
				{
469
					jQuery(app.classes.calendar.views[_view].etemplates[i].DOMContainer).hide();
470
				}
471
			}
472
			this.state.view = '';
473
			return true;
474
		}
475
		// can not load our own index page, has to be done by framework
476
		return false;
477
	},
478
479
	/**
480
	 * Handle actions from the toolbar
481
	 *
482
	 * @param {egwAction} action Action from the toolbar
483
	 */
484
	toolbar_action: function toolbar_action(action)
485
	{
486
		// Most can just provide state change data
487
		if(action.data && action.data.state)
488
		{
489
			var state = jQuery.extend({},action.data.state);
490
			if(state.view == 'planner' && app.calendar.state.view != 'planner') {
491
				state.planner_view = app.calendar.state.view;
492
			}
493
			this.update_state(state);
494
		}
495
		// Special handling
496
		switch(action.id)
0 ignored issues
show
Coding Style introduced by
As per coding-style, switch statements should have a default case.
Loading history...
497
		{
498
			case 'add':
499
				return egw.open(null,"calendar","add", {start: app.calendar.state.first});
500
			case 'weekend':
501
				this.update_state({weekend: action.checked});
502
				break;
503
			case 'today':
504
				var tempDate = new Date();
505
				var today = new Date(tempDate.getFullYear(), tempDate.getMonth(), tempDate.getDate(),0,-tempDate.getTimezoneOffset(),0);
506
				var change = {date: today.toJSON()};
507
				app.calendar.update_state(change);
508
				break;
509
			case 'next':
510
			case 'previous':
511
				var delta = action.id == 'previous' ? -1 : 1;
512
				var view = app.classes.calendar.views[app.calendar.state.view] || false;
513
				var start = new Date(app.calendar.state.date);
514
				if (view)
515
				{
516
					start = view.scroll(delta);
517
					app.calendar.update_state({date:app.calendar.date.toString(start)});
518
				}
519
				break;
520
		}
0 ignored issues
show
Comprehensibility introduced by
There is no default case in this switch, so nothing gets returned when all cases fail. You might want to consider adding a default or return undefined explicitly.
Loading history...
521
	},
522
523
	/**
524
	 * Set the app header
525
	 *
526
	 * Because the toolbar takes some vertical space and has some horizontal space,
527
	 * we don't use the system app header, but our own that is in the toolbar
528
	 *
529
	 * @param {string} header Text to display
530
	 */
531
	set_app_header: function(header) {
532
		var template = etemplate2.getById('calendar-toolbar');
533
		var widget = template ? template.widgetContainer.getWidgetById('app_header') : false;
534
		if(widget)
535
		{
536
			widget.set_value(header);
537
			egw_app_header('','calendar');
538
		}
539
		else
540
		{
541
			egw_app_header(header,'calendar');
542
		}
543
	},
544
545
	/**
546
	 * Setup and handle sortable calendars.
547
	 *
548
	 * You can only sort calendars if there is more than one owner, and the calendars
549
	 * are not combined (many owners, multi-week or month views)
550
	 * @returns {undefined}
551
	 */
552
	_sortable: function() {
553
		// Calender current state
554
		var state = this.getState();
555
		// Day / month sortables
556
		var daily = jQuery('#calendar-view_view .calendar_calGridHeader > div:first');
557
		var weekly = jQuery('#calendar-view_view tbody');
558
		if(state.view == 'day')
559
		{
560
			var sortable = daily;
561
			if(weekly.sortable('instance')) weekly.sortable('disable');
562
		}
563
		else
564
		{
565
			var sortable = weekly;
566
			if(daily.sortable('instance')) daily.sortable('disable');
567
		}
568
		if(!sortable.sortable('instance'))
569
		{
570
			sortable.sortable({
571
				cancel: "#divAppboxHeader, .calendar_calWeekNavHeader, .calendar_plannerHeader",
572
				handle: '.calendar_calGridHeader',
573
				//placeholder: "srotable_cal_wk_ph",
574
				axis:"y",
575
				revert: true,
576
				helper:"clone",
577
				create: function ()
578
				{
579
					var $sortItem = jQuery(this);
580
				},
581
				start: function (event, ui)
582
				{
583
					jQuery('.calendar_calTimeGrid',ui.helper).css('position', 'absolute');
584
					// Put owners into row IDs
585
					app.classes.calendar.views[app.calendar.state.view].etemplates[0].widgetContainer.iterateOver(function(widget) {
586
						if(widget.options.owner && !widget.disabled)
587
						{
588
							widget.div.parents('tr').attr('data-owner',widget.options.owner);
589
						}
590
						else
591
						{
592
							widget.div.parents('tr').removeAttr('data-owner');
593
						}
594
					},this,et2_calendar_timegrid);
595
				},
596
				stop: function ()
597
				{
598
				},
599
				update: function ()
600
				{
601
					var state = app.calendar.getState();
602
					if (state && typeof state.owner !== 'undefined')
603
					{
604
						var sortedArr = sortable.sortable('toArray', {attribute:"data-owner"});
605
						// No duplicates, no empties
606
						sortedArr = sortedArr.filter(function(value, index, self) {
607
							return value !== '' && self.indexOf(value) === index;
608
						});
609
610
						var parent = null;
611
						var children = [];
612
						if(state.view == 'day')
613
						{
614
							// If in day view, the days need to be re-ordered, avoiding
615
							// the current sort order
616
							app.classes.calendar.views.day.etemplates[0].widgetContainer.iterateOver(function(widget) {
617
								var idx = sortedArr.indexOf(widget.options.owner.toString());
618
								// Move the event holding div
619
								widget.set_left((parseInt(widget.options.width) * idx) + 'px');
620
								// Re-order the children, or it won't stay
621
								parent = widget._parent;
622
								children.splice(idx,0,widget);
623
							},this,et2_calendar_daycol);
624
							parent.day_widgets.sort(function(a,b) {
625
								return children.indexOf(a) - children.indexOf(b);
626
							});
627
						}
628
						else
629
						{
630
							// Re-order the children, or it won't stay
631
							app.classes.calendar.views.day.etemplates[0].widgetContainer.iterateOver(function(widget) {
632
								parent = widget._parent;
633
								var idx = sortedArr.indexOf(widget.options.owner);
634
								children.splice(idx,0,widget);
635
								widget.resize();
636
							},this,et2_calendar_timegrid);
637
						}
638
						parent._children.sort(function(a,b) {
639
							return children.indexOf(a) - children.indexOf(b);
640
						});
641
						// Directly update, since there is no other changes needed,
642
						// and we don't want the current sort order applied
643
						app.calendar.state.owner = sortedArr;
644
						parent.options.owner = sortedArr;
645
					}
646
				}
647
			});
648
		}
649
650
		// Enable or disable
651
		if(state.owner.length > 1 && (
652
			state.view == 'day' && state.owner.length < parseInt(egw.preference('day_consolidate','calendar')) ||
653
			state.view == 'week' && state.owner.length < parseInt(egw.preference('week_consolidate','calendar'))
654
		))
655
		{
656
			sortable.sortable('enable')
657
				.sortable("refresh")
658
				.disableSelection();
659
			var options = {};
660
			switch (state.view)
0 ignored issues
show
Coding Style introduced by
As per coding-style, switch statements should have a default case.
Loading history...
661
			{
662
				case "day":
663
					options = {
664
						placeholder:"srotable_cal_day_ph",
665
						axis:"x",
666
						handle: '> div:first',
667
						helper: function(event, element) {
668
							var scroll = element.parentsUntil('.calendar_calTimeGrid').last().next();
669
							var helper = jQuery(document.createElement('div'))
670
								.append(element.clone())
671
								.css('height',scroll.parent().css('height'))
672
								.css('background-color','white')
673
								.css('width', element.css('width'));
674
							return helper;
675
						}
676
					};
677
					sortable.sortable('option', options);
678
					break;
679
				case "week":
680
					options = {
681
						placeholder:"srotable_cal_wk_ph",
682
						axis:"y",
683
						handle: '.calendar_calGridHeader',
684
						helper: 'clone'
685
					};
686
					sortable.sortable('option', options);
687
					break;
688
			}
689
		}
690
		else
691
		{
692
			sortable.sortable('disable');
693
		}
694
	},
695
696
	/**
697
	 * Bind scroll event
698
	 * When the user scrolls, we'll move enddate - startdate days
699
	 */
700
	_scroll: function() {
701
		/**
702
		 * Function we can pass all this off to
703
		 *
704
		 * @param {String} direction up, down, left or right
705
		 * @param {number} delta Integer for how many we're moving, should be +/- 1
706
		 */
707
		var scroll_animate = function(direction, delta)
708
		{
709
			// Scrolling too fast?
710
			if(app.calendar._scroll_disabled) return;
711
712
			// Find the template
713
			var id = jQuery(this).closest('.et2_container').attr('id');
714
			if(id)
715
			{
716
				var template = etemplate2.getById(id);
717
			}
718
			else
719
			{
720
				template = app.classes.calendar.views[app.calendar.state.view].etemplates[0];
721
			}
722
			if(!template) return;
723
724
			// Prevent scrolling too fast
725
			app.calendar._scroll_disabled = true;
726
727
			// Animate the transition, if possible
728
			var widget = null;
729
			template.widgetContainer.iterateOver(function(w) {
730
				if (w.getDOMNode() == this) widget = w;
731
			},this,et2_widget);
732
			if(widget == null)
733
			{
734
				template.widgetContainer.iterateOver(function(w) {
735
					widget = w;
736
				},this, et2_calendar_timegrid);
737
				if(widget == null) return;
738
			}
739
			/* Disabled
740
			 *
741
			// We clone the nodes so we can animate the transition
742
			var original = jQuery(widget.getDOMNode()).closest('.et2_grid');
743
			var cloned = original.clone(true).attr("id","CLONE");
744
745
			// Moving this stuff around scrolls things around too
746
			// We need this later
747
			var scrollTop = jQuery('.calendar_calTimeGridScroll',original).scrollTop();
748
749
			// This is to hide the scrollbar
750
			var wrapper = original.parent();
751
			if(direction == "right" || direction == "left")
752
			{
753
				original.css({"display":"inline-block","width":original.width()+"px"});
754
				cloned.css({"display":"inline-block","width":original.width()+"px"});
755
			}
756
			else
757
			{
758
				original.css("height",original.height() + "px");
759
				cloned.css("height",original.height() + "px");
760
			}
761
			var original_size = {height: wrapper.parent().css('height'), width: wrapper.parent().css('width')};
762
			wrapper.parent().css({overflow:'hidden', height:original.outerHeight()+"px", width:original.outerWidth() + "px"});
763
			wrapper.height(direction == "up" || direction == "down" ? 2 * original.outerHeight()  : original.outerHeight());
764
			wrapper.width(direction == "left" || direction == "right" ? 2 * original.outerWidth() : original.outerWidth());
765
766
			// Re-scroll to previous to avoid "jumping"
767
			jQuery('.calendar_calTimeGridScroll',original).scrollTop(scrollTop);
768
			switch(direction)
769
			{
770
				case "up":
771
				case "left":
772
					// Scrolling up
773
					// Apply the reverse quickly, then let it animate as the changes are
774
					// removed, leaving things where they should be.
775
776
					original.parent().append(cloned);
777
					// Makes it jump to destination
778
					wrapper.css({
779
						"transition-duration": "0s",
780
						"transition-delay": "0s",
781
						"transform": direction == "up" ? "translateY(-50%)" : "translateX(-50%)"
782
					});
783
					// Stop browser from caching style by forcing reflow
784
					if(wrapper[0]) wrapper[0].offsetHeight;
785
786
					wrapper.css({
787
						"transition-duration": "",
788
						"transition-delay": ""
789
					});
790
					break;
791
				case "down":
792
				case "right":
793
					// Scrolling down
794
					original.parent().prepend(cloned);
795
					break;
796
			}
797
			// Scroll clone to match to avoid "jumping"
798
			jQuery('.calendar_calTimeGridScroll',cloned).scrollTop(scrollTop);
799
800
			// Remove
801
			var remove = function() {
802
				// Starting animation
803
				wrapper.addClass("calendar_slide");
804
				var translate = direction == "down" ? "translateY(-50%)" : (direction == "right" ? "translateX(-50%)" : "");
805
				wrapper.css({"transform": translate});
806
				window.setTimeout(function() {
807
808
					cloned.remove();
809
810
					// Makes it jump to destination
811
					wrapper.css({
812
						"transition-duration": "0s",
813
						"transition-delay": "0s"
814
					});
815
816
					// Clean up from animation
817
					wrapper
818
						.removeClass("calendar_slide")
819
						.css({"transform": '',height: '', width:'',overflow:''});
820
					wrapper.parent().css({overflow: '', width: original_size.width, height: original_size.height});
821
					original.css("display","");
822
					if(wrapper.length)
823
					{
824
						wrapper[0].offsetHeight;
825
					}
826
					wrapper.css({
827
						"transition-duration": "",
828
						"transition-delay": ""
829
					});
830
831
					// Re-scroll to start of day
832
					template.widgetContainer.iterateOver(function(w) {
833
						w.resizeTimes();
834
					},this, et2_calendar_timegrid);
835
836
					window.setTimeout(function() {
837
						if(app.calendar)
838
						{
839
							app.calendar._scroll_disabled = false;
840
						}
841
					}, 100);
842
				},2000);
843
			}
844
			// If detecting the transition end worked, we wouldn't need to use a timeout.
845
			window.setTimeout(remove,100);
846
			*/
847
		   window.setTimeout(function() {
848
				if(app.calendar)
849
				{
850
					app.calendar._scroll_disabled = false;
851
				}
852
			}, 2000);
853
			// Get the view to calculate - this actually loads the new data
854
			// Using a timeout make it a little faster (in Chrome)
855
			window.setTimeout(function() {
856
				var view = app.classes.calendar.views[app.calendar.state.view] || false;
857
				var start = new Date(app.calendar.state.date);
858
				if (view && view.etemplates.indexOf(template) !== -1)
859
				{
860
					start = view.scroll(delta);
861
					app.calendar.update_state({date:app.calendar.date.toString(start)});
862
				}
863
				else
864
				{
865
					// Home - always 1 week
866
					// TODO
867
					return false;
868
				}
869
			},0);
870
		};
871
872
		// Bind only once, to the whole thing
873
		/* Disabled
874
		jQuery('body').off('.calendar')
875
			//.on('wheel','.et2_container:#calendar-list,#calendar-sidebox)',
876
			.on('wheel.calendar','.et2_container .calendar_calTimeGrid, .et2_container .calendar_plannerWidget',
877
				function(e)
878
				{
879
					// Consume scroll if in the middle of something
880
					if(app.calendar._scroll_disabled) return false;
881
882
					// Ignore if they're going the other way
883
					var direction = e.originalEvent.deltaY > 0 ? 1 : -1;
884
					var at_bottom = direction !== -1;
885
					var at_top = direction !== 1;
886
887
					jQuery(this).children(":not(.calendar_calGridHeader)").each(function() {
888
						// Check for less than 2px from edge, as sometimes we can't scroll anymore, but still have
889
						// 2px left to go
890
						at_bottom = at_bottom && Math.abs(this.scrollTop - (this.scrollHeight - this.offsetHeight)) <= 2;
891
					}).each(function() {
892
						at_top = at_top && this.scrollTop === 0;
893
					});
894
					if(!at_bottom && !at_top) return;
895
896
					e.preventDefault();
897
898
					scroll_animate.call(this, direction > 0 ? "down" : "up", direction);
899
900
					return false;
901
				}
902
			);
903
		*/
904
		if(typeof framework !== 'undefined' && framework.applications.calendar && framework.applications.calendar.tab)
905
		{
906
			jQuery(framework.applications.calendar.tab.contentDiv)
907
				.swipe('destroy');
908
909
			jQuery(framework.applications.calendar.tab.contentDiv)
910
				.swipe({
911
					//Generic swipe handler for all directions
912
					swipe:function(event, direction, distance, duration, fingerCount) {
913
						if(direction == "up" || direction == "down")
914
						{
915
							if(fingerCount <= 1) return;
916
							var at_bottom = direction !== -1;
917
							var at_top = direction !== 1;
918
919
							jQuery(this).children(":not(.calendar_calGridHeader)").each(function() {
920
								// Check for less than 2px from edge, as sometimes we can't scroll anymore, but still have
921
								// 2px left to go
922
								at_bottom = at_bottom && Math.abs(this.scrollTop - (this.scrollHeight - this.offsetHeight)) <= 2;
923
							}).each(function() {
924
								at_top = at_top && this.scrollTop === 0;
925
							});
926
						}
927
928
						var delta = direction == "down" || direction == "right" ? -1 : 1;
929
						// But we animate in the opposite direction to the swipe
930
						var opposite = {"down": "up", "up": "down", "left": "right", "right": "left"};
931
						direction = opposite[direction];
932
						scroll_animate.call(jQuery(event.target).closest('.calendar_calTimeGrid, .calendar_plannerWidget')[0], direction, delta);
933
						return false;
934
					},
935
					allowPageScroll: jQuery.fn.swipe.pageScroll.VERTICAL,
936
					threshold: 100,
937
					fallbackToMouseEvents: false,
938
					triggerOnTouchEnd: false
939
				});
940
941
			// Page up & page down
942
			egw_registerGlobalShortcut(jQuery.ui.keyCode.PAGE_UP, false, false, false, function() {
943
				if(app.calendar.state.view == 'listview')
944
				{
945
					return false;
946
				}
947
				scroll_animate.call(this,"up", -1);
948
				return true;
949
			});
950
			egw_registerGlobalShortcut(jQuery.ui.keyCode.PAGE_DOWN, false, false, false, function() {
951
				if(app.calendar.state.view == 'listview')
952
				{
953
					return false;
954
				}
955
				scroll_animate.call(this,"down", 1);
956
				return true;
957
			});
958
		}
959
	},
960
961
	/**
962
	 * Handler for changes generated by internal user interactions, like
963
	 * drag & drop inside calendar and resize.
964
	 *
965
	 * @param {Event} event
966
	 * @param {et2_calendar_event} widget Widget for the event
967
	 * @param {string} dialog_button - 'single', 'series', or 'exception', based on the user's answer
968
	 *	in the popup
969
	 * @returns {undefined}
970
	 */
971
	event_change: function(event, widget, dialog_button)
972
	{
973
		// Add loading spinner - not visible if the body / gradient is there though
974
		widget.div.addClass('loading');
975
976
		// Integrated infolog event
977
		//Get infologID if in case if it's an integrated infolog event
978
		if (widget.options.value.app == 'infolog')
979
		{
980
			// If it is an integrated infolog event we need to edit infolog entry
981
			egw().json(
982
				'stylite_infolog_calendar_integration::ajax_moveInfologEvent',
983
				[widget.options.value.app_id, widget.options.value.start, widget.options.value.duration],
984
				// Remove loading spinner
985
				function() {if(widget.div) widget.div.removeClass('loading');}
986
			).sendRequest();
987
		}
988
		else
989
		{
990
			var _send = function() {
991
				egw().json(
992
					'calendar.calendar_uiforms.ajax_moveEvent',
993
					[
994
						dialog_button == 'exception' ? widget.options.value.app_id : widget.options.value.id,
995
						widget.options.value.owner,
996
						widget.options.value.start,
997
						widget.options.value.owner,
998
						widget.options.value.duration,
999
						dialog_button == 'series' ? widget.options.value.start : null
1000
					],
1001
					// Remove loading spinner
1002
					function() {if(widget && widget.div) widget.div.removeClass('loading');}
1003
				).sendRequest(true);
1004
			};
1005
			if(dialog_button == 'series' && widget.options.value.recur_type)
1006
			{
1007
				widget.series_split_prompt(function(_button_id)
1008
					{
1009
						if (_button_id == et2_dialog.OK_BUTTON)
1010
						{
1011
							_send();
1012
						}
1013
					}
1014
				);
1015
			}
1016
			else
1017
			{
1018
				_send();
1019
			}
1020
		}
1021
	},
1022
1023
	/**
1024
	 * open the freetime search popup
1025
	 *
1026
	 * @param {string} _link
1027
	 */
1028
	freetime_search_popup: function(_link)
1029
	{
1030
		this.egw.open_link(_link,'ft_search','700x500') ;
1031
	},
1032
1033
	/**
1034
	 * send an ajax request to server to set the freetimesearch window content
1035
	 *
1036
	 */
1037
	freetime_search: function()
1038
	{
1039
		var content = this.et2.getArrayMgr('content').data;
1040
		content['start'] = this.et2.getWidgetById('start').get_value();
1041
		content['end'] = this.et2.getWidgetById('end').get_value();
1042
		content['duration'] = this.et2.getWidgetById('duration').get_value();
1043
1044
		var request = this.egw.json('calendar.calendar_uiforms.ajax_freetimesearch', [content],null,null,null,null);
1045
		request.sendRequest();
1046
	},
1047
1048
	/**
1049
	 * Function for disabling the recur_data multiselect box
1050
	 *
1051
	 */
1052
	check_recur_type: function()
1053
	{
1054
		var recurType = this.et2.getWidgetById('recur_type');
1055
		var recurData = this.et2.getWidgetById('recur_data');
1056
1057
		if(recurType && recurData)
1058
		{
1059
			recurData.set_disabled(recurType.get_value() != 2 && recurType.get_value() != 4);
1060
		}
1061
	},
1062
1063
	/**
1064
	 * Actions for when the user changes the event start date in edit dialog
1065
	 *
1066
	 * @returns {undefined}
1067
	 */
1068
	edit_start_change: function(input, widget)
1069
	{
1070
		if(!widget)
1071
		{
1072
			widget = etemplate2.getById('calendar-edit').widgetContainer.getWidgetById('start');
1073
		}
1074
1075
		// Update settings for querying participants
1076
		this.edit_update_participant(widget);
1077
1078
		// Update recurring date limit, if not set it can't be before start
1079
		if(widget)
1080
		{
1081
			var recur_end = widget.getRoot().getWidgetById('recur_enddate');
1082
			if(recur_end && recur_end.getValue && !recur_end.getValue())
1083
			{
1084
				recur_end.set_min(widget.getValue());
1085
			}
1086
		}
1087
		// Update currently selected alarm time
1088
		this.alarm_custom_date();
1089
	},
1090
1091
	/**
1092
	 * Show/Hide end date, for both edit and freetimesearch popups,
1093
	 * based on if "use end date" selected or not.
1094
	 *
1095
	 */
1096
	set_enddate_visibility: function()
1097
	{
1098
		var duration = this.et2.getWidgetById('duration');
1099
		var start = this.et2.getWidgetById('start');
1100
		var end = this.et2.getWidgetById('end');
1101
		var content = this.et2.getArrayMgr('content').data;
1102
1103
		if (typeof duration != 'undefined' && typeof end != 'undefined')
1104
		{
1105
			end.set_disabled(duration.get_value()!=='');
1106
1107
			// Only set end date if not provided, adding seconds fails with DST
1108
			if (!end.disabled && !content.end)
1109
			{
1110
				end.set_value(start.get_value());
1111
				if (typeof content.duration != 'undefined') end.set_value("+"+content.duration);
1112
			}
1113
		}
1114
		this.edit_update_participant(start);
1115
	},
1116
1117
	/**
1118
	 * Update query parameters for participants
1119
	 *
1120
	 * This allows for resource conflict checking
1121
	 *
1122
	 * @param {DOMNode|et2_widget} input Either the input node, or the widget
1123
	 * @param {et2_widget} [widget] If input is an input node, widget will have
1124
	 *	the widget, otherwise it will be undefined.
1125
	 */
1126
	edit_update_participant: function(input, widget)
1127
	{
1128
		if(typeof widget === 'undefined') widget = input;
1129
		var content = widget.getInstanceManager().getValues(widget.getRoot());
1130
		var participant = widget.getRoot().getWidgetById('participant');
1131
		if(!participant) return;
1132
1133
		participant.set_autocomplete_params({exec:{
1134
			start: content.start,
1135
			end: content.end,
1136
			duration: content.duration,
1137
			whole_day: content.whole_day,
1138
		}});
1139
	},
1140
1141
	/**
1142
	 * handles actions selectbox in calendar edit popup
1143
	 *
1144
	 * @param {mixed} _event
1145
	 * @param {et2_base_widget} widget "actions selectBox" in edit popup window
1146
	 */
1147
	actions_change: function(_event, widget)
1148
	{
1149
		var event = this.et2.getArrayMgr('content').data;
1150
		if (widget)
1151
		{
1152
			var id = this.et2.getArrayMgr('content').data['id'];
1153
			switch (widget.get_value())
1154
			{
1155
				case 'print':
1156
					this.egw.open_link('calendar.calendar_uiforms.edit&cal_id='+id+'&print=1','_blank','700x700');
1157
					break;
1158
				case 'mail':
1159
					this.egw.json('calendar.calendar_uiforms.ajax_custom_mail', [event, !event['id'], false],null,null,null,null).sendRequest();
1160
					this.et2._inst.submit();
1161
					break;
1162
				case 'sendrequest':
1163
					this.egw.json('calendar.calendar_uiforms.ajax_custom_mail', [event, !event['id'], true],null,null,null,null).sendRequest();
1164
					this.et2._inst.submit();
1165
					break;
1166
				case 'infolog':
1167
					this.egw.open_link('infolog.infolog_ui.edit&action=calendar&action_id='+(jQuery.isPlainObject(event)?event['id']:event),'_blank','700x600','infolog');
1168
					this.et2._inst.submit();
1169
					break;
1170
				case 'ical':
1171
					this.et2._inst.postSubmit();
1172
					break;
1173
				default:
1174
					this.et2._inst.submit();
1175
			}
1176
		}
1177
	},
1178
1179
	/**
1180
	 * open mail compose popup window
1181
	 *
1182
	 * @param {Array} vars
1183
	 * @todo need to provide right mail compose from server to custom_mail function
1184
	 */
1185
	custom_mail: function (vars)
1186
	{
1187
		this.egw.open_link(this.egw.link("/index.php",vars),'_blank','700x700');
1188
	},
1189
1190
	/**
1191
	 * control delete_series popup visibility
1192
	 *
1193
	 * @param {et2_widget} widget
1194
	 * @param {Array} exceptions an array contains number of exception entries
1195
	 *
1196
	 */
1197
	delete_btn: function(widget,exceptions)
1198
	{
1199
		var content = this.et2.getArrayMgr('content').data;
1200
1201
		if (exceptions)
1202
		{
1203
			var buttons = [
1204
				{
1205
					button_id: 'keep',
1206
					title: this.egw.lang('All exceptions are converted into single events.'),
1207
					text: this.egw.lang('Keep exceptions'),
1208
					id: 'button[delete_keep_exceptions]',
1209
					image: 'keep', "default":true
1210
				},
1211
				{
1212
					button_id: 'delete',
1213
					title: this.egw.lang('The exceptions are deleted together with the series.'),
1214
					text: this.egw.lang('Delete exceptions'),
1215
					id: 'button[delete_exceptions]',
1216
					image: 'delete'
1217
				},
1218
				{
1219
					button_id: 'cancel',
1220
					text: this.egw.lang('Cancel'),
1221
					id: 'dialog[cancel]',
1222
					image: 'cancel'
1223
				}
1224
1225
			];
1226
			var self = this;
1227
			et2_dialog.show_dialog
1228
			(
1229
					function(_button_id)
1230
					{
1231
						if (_button_id != 'dialog[cancel]')
1232
						{
1233
							widget.getRoot().getWidgetById('delete_exceptions').set_value(_button_id == 'button[delete_exceptions]');
1234
							widget.getInstanceManager().submit('button[delete]');
1235
							return true;
1236
						}
1237
						else
1238
						{
1239
							return false;
1240
						}
1241
					},
1242
					this.egw.lang("Do you want to keep the series exceptions in your calendar?"),
1243
					this.egw.lang("This event is part of a series"), {}, buttons , et2_dialog.WARNING_MESSAGE
1244
			);
1245
		}
1246
		else if (content['recur_type'] !== 0)
1247
		{
1248
			et2_dialog.confirm(widget,'Delete this series of recuring events','Delete Series');
1249
		}
1250
		else
1251
		{
1252
			et2_dialog.confirm(widget,'Delete this event','Delete');
1253
		}
1254
	},
1255
1256
	/**
1257
	 * On change participant event, try to set add button status based on
1258
	 * participant field value. Additionally, disable/enable quantity field
1259
	 * if there's none resource value or there are more than one resource selected.
1260
	 *
1261
	 */
1262
	participantOnChange: function ()
1263
	{
1264
		var add = this.et2.getWidgetById('add');
1265
		var quantity = this.et2.getWidgetById('quantity');
1266
		var participant = this.et2.getWidgetById('participant');
1267
1268
		// array of participants
1269
		var value = participant.get_value();
1270
1271
		add.set_readonly(value.length <= 0);
1272
1273
		quantity.set_readonly(false);
1274
1275
		// number of resources
1276
		var nRes = 0;
1277
1278
		for (var i=0;i<value.length;i++)
1279
		{
1280
			if (!value[i].match(/\D/ig) || nRes)
1281
			{
1282
				quantity.set_readonly(true);
1283
				quantity.set_value(1);
1284
			}
1285
			nRes++;
1286
		}
1287
	},
1288
1289
	/**
1290
	 * print_participants_status(egw,widget)
1291
	 * Handle to apply changes from status in print popup
1292
	 *
1293
	 * @param {mixed} _event
1294
	 * @param {et2_base_widget} widget widget "status" in print popup window
1295
	 *
1296
	 */
1297
	print_participants_status: function(_event, widget)
1298
	{
1299
		if (widget && window.opener)
1300
		{
1301
			//Parent popup window
1302
			var editPopWindow = window.opener;
1303
1304
			if (editPopWindow)
1305
			{
1306
				//Update paretn popup window
1307
				editPopWindow.etemplate2.getByApplication('calendar')[0].widgetContainer.getWidgetById(widget.id).set_value(widget.get_value());
1308
			}
1309
			this.et2._inst.submit();
1310
1311
			editPopWindow.opener.egw_refresh('status changed','calendar');
1312
		}
1313
		else if (widget)
1314
		{
1315
			window.egw_refresh(this.egw.lang('The original popup edit window is closed! You need to close the print window and reopen the entry again.'),'calendar');
1316
		}
1317
	},
1318
1319
	/**
1320
	 * Handles to select freetime, and replace the selected one on Start,
1321
	 * and End date&time in edit calendar entry popup.
1322
	 *
1323
	 * @param {mixed} _event
1324
	 * @param {et2_base_widget} _widget widget "select button" in freetime search popup window
1325
	 *
1326
	 */
1327
	freetime_select: function(_event, _widget)
1328
	{
1329
		if (_widget)
1330
		{
1331
			var content = this.et2._inst.widgetContainer.getArrayMgr('content').data;
1332
			// Make the Id from selected button by checking the index
1333
			var selectedId = _widget.id.match(/^select\[([0-9])\]$/i)[1];
1334
1335
			var sTime = this.et2.getWidgetById(selectedId+'start');
1336
1337
			//check the parent window is still open before to try to access it
1338
			if (window.opener && sTime)
1339
			{
1340
				var editWindowObj = window.opener.etemplate2.getByApplication('calendar')[0];
1341
				if (typeof editWindowObj != "undefined")
1342
				{
1343
					var startTime = editWindowObj.widgetContainer.getWidgetById('start');
1344
					var endTime = editWindowObj.widgetContainer.getWidgetById('end');
1345
					if (startTime && endTime)
1346
					{
1347
						startTime.set_value(sTime.get_value());
1348
						endTime.set_value(sTime.get_value());
1349
						endTime.set_value('+'+content['duration']);
1350
					}
1351
				}
1352
			}
1353
			else
1354
			{
1355
				alert(this.egw.lang('The original calendar edit popup is closed!'));
1356
			}
1357
		}
1358
		egw(window).close();
1359
	},
1360
1361
	/**
1362
	 * show/hide the filter of nm list in calendar listview
1363
	 *
1364
	 */
1365
	filter_change: function()
1366
	{
1367
		var view = app.classes.calendar.views['listview'].etemplates[0].widgetContainer || false;
1368
		var filter = view ? view.getWidgetById('nm').getWidgetById('filter') : null;
1369
		var dates = view ? view.getWidgetById('calendar.list.dates') : null;
1370
1371
		// Update state when user changes it
1372
		if(filter)
1373
		{
1374
			app.calendar.state.filter = filter.getValue();
1375
			// Change sort order for before - this is just the UI, server does the query
1376
			if(app.calendar.state.filter == 'before')
1377
			{
1378
				view.getWidgetById('nm').sortBy('cal_start',false, false);
1379
			}
1380
			else
1381
			{
1382
				view.getWidgetById('nm').sortBy('cal_start',true, false);
1383
			}
1384
		}
1385
		else
1386
		{
1387
			delete app.calendar.state.filter;
1388
		}
1389
		if (filter && dates)
1390
		{
1391
			dates.set_disabled(filter.value !== "custom");
1392
			if (filter.value == "custom" && !this.state_update_in_progress)
1393
			{
1394
				// Copy state dates over, without causing [another] state update
1395
				var actual = this.state_update_in_progress;
1396
				this.state_update_in_progress = true;
1397
				view.getWidgetById('startdate').set_value(app.calendar.state.first);
1398
				view.getWidgetById('enddate').set_value(app.calendar.state.last);
1399
				this.state_update_in_progress = actual;
1400
1401
				jQuery(view.getWidgetById('startdate').getDOMNode()).find('input').focus();
1402
			}
1403
		}
1404
	},
1405
1406
	/**
1407
	 * Application links from non-list events
1408
	 *
1409
	 * The ID looks like calendar::<id> or calendar::<id>:<recurrence_date>
1410
	 * For processing the links:
1411
	 *	'$app' gets replaced with 'calendar'
1412
	 *	'$id' gets replaced with <id>
1413
	 *	'$app_id gets replaced with <id>:<recurrence_date>
1414
	 *
1415
	 * Use either $id or $app_id depending on if you want the series [beginning]
1416
	 * or a particular date.
1417
	 *
1418
	 * @param {egwAction} _action
1419
	 * @param {egwActionObject[]} _events
1420
	 */
1421
	action_open: function(_action, _events)
1422
	{
1423
		var id = _events[0].id.split('::');
1424
		var app = id[0];
1425
		var app_id = id[1];
1426
		if(app_id.indexOf(':'))
1427
		{
1428
			var split = id[1].split(':');
1429
			id = split[0];
1430
		}
1431
		else
1432
		{
1433
			id = app_id;
1434
		}
1435
		if(_action.data.open)
1436
		{
1437
			var open = JSON.parse(_action.data.open) || {};
1438
			var extra = open.extra || '';
1439
1440
			extra = extra.replace(/(\$|%24)app/,app).replace(/(\$|%24)app_id/,app_id)
1441
					.replace(/(\$|%24)id/,id);
1442
1443
			// Get a little smarter with the context
1444
			if(!extra)
1445
			{
1446
				var context = {};
1447
				if(egw.dataGetUIDdata(_events[0].id) && egw.dataGetUIDdata(_events[0].id).data)
1448
				{
1449
					// Found data in global cache
1450
					context = egw.dataGetUIDdata(_events[0].id).data;
1451
					extra = {};
1452
				}
1453
				else if (_events[0].iface.getWidget() && _events[0].iface.getWidget().instanceOf(et2_valueWidget))
1454
				{
1455
					// Able to extract something from the widget
1456
					context = _events[0].iface.getWidget().getValue ?
1457
						_events[0].iface.getWidget().getValue() :
1458
						_events[0].iface.getWidget().options.value || {};
1459
					extra = {};
1460
				}
1461
				// Try to pull whatever we can from the event
1462
				else if (jQuery.isEmptyObject(context) && _action.menu_context && (_action.menu_context.event.target))
1463
				{
1464
					var target = _action.menu_context.event.target;
1465
					while(target != null && target.parentNode && jQuery.isEmptyObject(target.dataset))
1466
					{
1467
						target = target.parentNode;
1468
					}
1469
1470
					context = extra = jQuery.extend({},target.dataset);
1471
					var owner = jQuery(target).closest('[data-owner]').get(0);
1472
					if(owner && owner.dataset.owner && owner.dataset.owner != this.state.owner)
1473
					{
1474
						extra.owner = owner.dataset.owner.split(',');
1475
					}
1476
				}
1477
				if(context.date) extra.date = context.date;
1478
				if(context.app) extra.app = context.app;
1479
				if(context.app_id) extra.app_id = context.app_id;
1480
			}
1481
1482
			this.egw.open(open.id_data||'',open.app,open.type,extra ? extra : context);
0 ignored issues
show
Bug introduced by
The variable context does not seem to be initialized in case !extra on line 1444 is false. Are you sure this can never be the case?
Loading history...
1483
		}
1484
		else if (_action.data.url)
1485
		{
1486
			var url = _action.data.url;
1487
			url = url.replace(/(\$|%24)app/,app).replace(/(\$|%24)app_id/,app_id)
1488
					.replace(/(\$|%24)id/,id);
1489
			this.egw.open_link(url);
1490
		}
1491
	},
1492
1493
	/**
1494
	 * Context menu action (on a single event) in non-listview to generate ical
1495
	 *
1496
	 * Since nextmatch is all ready to handle that, we pass it through
1497
	 *
1498
	 * @param {egwAction} _action
1499
	 * @param {egwActionObject[]} _events
1500
	 */
1501
	ical: function(_action, _events)
1502
	{
1503
		// Send it through nextmatch
1504
		_action.data.nextmatch = etemplate2.getById('calendar-list').widgetContainer.getWidgetById('nm');
1505
		var ids = {ids:[]};
1506
		for(var i = 0; i < _events.length; i++)
1507
		{
1508
			ids.ids.push(_events[i].id);
1509
		}
1510
		nm_action(_action, _events, null, ids);
1511
	},
1512
1513
	/**
1514
	 * Change status (via AJAX)
1515
	 *
1516
	 * @param {egwAction} _action
1517
	 * @param {egwActionObject} _events
1518
	 */
1519
	status: function(_action, _events)
1520
	{
1521
		// Should be a single event, but we'll do it for all
1522
		for(var i = 0; i < _events.length; i++)
1523
		{
1524
			var event_widget = _events[i].iface.getWidget() || false;
1525
			if(!event_widget) continue;
1526
1527
			event_widget.recur_prompt(jQuery.proxy(function(button_id,event_data) {
1528
				switch(button_id)
1529
				{
1530
					case 'exception':
1531
						egw().json(
1532
							'calendar.calendar_uiforms.ajax_status',
1533
							[event_data.app_id, egw.user('account_id'), _action.data.id]
1534
						).sendRequest(true);
1535
						break;
1536
					case 'series':
1537
					case 'single':
1538
						egw().json(
1539
							'calendar.calendar_uiforms.ajax_status',
1540
							[event_data.id, egw.user('account_id'), _action.data.id]
1541
						).sendRequest(true);
1542
						break;
1543
					case 'cancel':
1544
					default:
1545
						break;
1546
				}
1547
			},this));
1548
		}
1549
1550
	},
1551
1552
	/**
1553
	 * this function try to fix ids which are from integrated apps
1554
	 *
1555
	 * @param {egwAction} _action
1556
	 * @param {egwActionObject[]} _senders
1557
	 */
1558
	cal_fix_app_id: function(_action, _senders)
1559
	{
1560
		var app = 'calendar';
1561
		var id = _senders[0].id;
1562
		var matches = id.match(/^(?:calendar::)?([0-9]+)(:([0-9]+))?$/);
1563
		if (matches)
1564
		{
1565
			id = matches[1];
1566
		}
1567
		else
1568
		{
1569
			matches = id.match(/^([a-z_-]+)([0-9]+)/i);
1570
			if (matches)
1571
			{
1572
				app = matches[1];
1573
				id = matches[2];
1574
			}
1575
		}
1576
		var backup_url = _action.data.url;
1577
1578
		_action.data.url = _action.data.url.replace(/(\$|%24)id/,id);
1579
		_action.data.url = _action.data.url.replace(/(\$|%24)app/,app);
1580
1581
		nm_action(_action, _senders,false,{ids:[id]});
1582
1583
		_action.data.url = backup_url;	// restore url
1584
	},
1585
1586
	/**
1587
	 * Open calendar entry, taking into accout the calendar integration of other apps
1588
	 *
1589
	 * calendar_uilist::get_rows sets var js_calendar_integration object
1590
	 *
1591
	 * @param _action
1592
	 * @param _senders
1593
	 *
1594
	 */
1595
	cal_open: function(_action, _senders)
1596
	{
1597
		// Try for easy way - find a widget
1598
		if(_senders[0].iface.getWidget)
1599
		{
1600
			var widget = _senders[0].iface.getWidget();
1601
			return widget.recur_prompt();
1602
		}
1603
1604
		// Nextmatch in list view does not have a widget, but we can pull
1605
		// the data by ID
1606
		// Check for series
1607
		var id = _senders[0].id;
1608
		var data = egw.dataGetUIDdata(id);
1609
		if (data && data.data)
1610
		{
1611
			et2_calendar_event.recur_prompt(data.data);
1612
			return;
1613
		}
1614
		var matches = id.match(/^(?:calendar::)?([0-9]+):([0-9]+)$/);
1615
1616
		// Check for other app integration data sent from server
1617
		var backup = _action.data;
1618
		if(_action.parent.data && _action.parent.data.nextmatch)
1619
		{
1620
			var js_integration_data = _action.parent.data.nextmatch.options.settings.js_integration_data || this.et2.getArrayMgr('content').data.nm.js_integration_data;
1621
			if(typeof js_integration_data == 'string')
1622
			{
1623
				js_integration_data = JSON.parse(js_integration_data);
1624
			}
1625
		}
1626
		matches = id.match(/^calendar::([a-z_-]+)([0-9]+)/i);
1627
		if (matches && js_integration_data && js_integration_data[matches[1]])
1628
		{
1629
			var app = matches[1];
1630
			_action.data.url = window.egw_webserverUrl+'/index.php?';
1631
			var get_params = js_integration_data[app].edit;
1632
			get_params[js_integration_data[app].edit_id] = matches[2];
1633
			for(var name in get_params)
1634
				_action.data.url += name+"="+encodeURIComponent(get_params[name])+"&";
1635
1636
			if (js_integration_data[app].edit_popup)
1637
			{
1638
				egw.open_link(_action.data.url,'_blank',js_integration_data[app].edit_popup,app);
1639
1640
				_action.data = backup;	// restore url, width, height, nm_action
1641
				return;
1642
			}
1643
		}
1644
		else
1645
		{
1646
			// Other app integration using link registry
1647
			var data = egw.dataGetUIDdata(_senders[0].id);
1648
			if(data && data.data)
1649
			{
1650
				return egw.open(data.data.app_id, data.data.app, 'edit');
1651
			}
1652
		}
1653
		// Regular, single event
1654
		egw.open(id.replace(/^calendar::/g,''),'calendar','edit');
1655
	},
1656
1657
	/**
1658
	 * Delete (a single) calendar entry over ajax.
1659
	 *
1660
	 * Used for the non-list views
1661
	 *
1662
	 * @param {egwAction} _action
1663
	 * @param {egwActionObject} _events
1664
	 */
1665
	delete: function(_action, _events)
1666
	{
1667
		// Should be a single event, but we'll do it for all
1668
		for(var i = 0; i < _events.length; i++)
1669
		{
1670
			var event_widget = _events[i].iface.getWidget() || false;
1671
			if(!event_widget) continue;
1672
1673
			event_widget.recur_prompt(jQuery.proxy(function(button_id,event_data) {
1674
				switch(button_id)
1675
				{
1676
					case 'exception':
1677
						egw().json(
1678
							'calendar.calendar_uiforms.ajax_delete',
1679
							[event_data.app_id]
1680
						).sendRequest(true);
1681
						break;
1682
					case 'series':
1683
					case 'single':
1684
						egw().json(
1685
							'calendar.calendar_uiforms.ajax_delete',
1686
							[event_data.id]
1687
						).sendRequest(true);
1688
						break;
1689
					case 'cancel':
1690
					default:
1691
						break;
1692
				}
1693
			},this));
1694
		}
1695
	},
1696
1697
	/**
1698
	 * Delete calendar entry, asking if you want to delete series or exception
1699
	 *
1700
	 * Used for nextmatch
1701
	 *
1702
	 * @param _action
1703
	 * @param _senders
1704
	 */
1705
	cal_delete: function(_action, _senders)
1706
	{
1707
		var backup = _action.data;
1708
		var matches = false;
1709
1710
		// Loop so we ask if any of the selected entries is part of a series
1711
		for(var i = 0; i < _senders.length; i++)
1712
		{
1713
			var id = _senders[i].id;
1714
			if(!matches)
1715
			{
1716
				matches = id.match(/^(?:calendar::)?([0-9]+):([0-9]+)$/);
1717
			}
1718
		}
1719
		if (matches)
1720
		{
1721
			var popup = jQuery('#calendar-list_delete_popup').get(0);
1722
			if (typeof popup != 'undefined')
1723
			{
1724
				// nm action - show popup
1725
				nm_open_popup(_action,_senders);
1726
			}
1727
			return;
1728
		}
1729
1730
		nm_action(_action, _senders);
1731
	},
1732
1733
	/**
1734
	 * Confirmation dialog for moving a series entry
1735
	 *
1736
	 * @param {object} _DOM
1737
	 * @param {et2_widget} _button button Save | Apply
1738
	 */
1739
	move_edit_series: function(_DOM,_button)
1740
	{
1741
		var content = this.et2.getArrayMgr('content').data;
1742
		var start_date = this.et2.getWidgetById('start').get_value();
1743
		var end_date = this.et2.getWidgetById('end').get_value();
1744
		var whole_day = this.et2.getWidgetById('whole_day');
1745
		var duration = ''+this.et2.getWidgetById('duration').get_value();
1746
		var is_whole_day = whole_day && whole_day.get_value() == whole_day.options.selected_value;
1747
		var button = _button;
1748
		var that = this;
1749
1750
		var instance_date = window.location.search.match(/date=(\d{4}-\d{2}-\d{2}(?:.+Z)?)/);
1751
		if(instance_date && instance_date.length && instance_date[1])
1752
		{
1753
			instance_date = new Date(unescape(instance_date[1]));
1754
			instance_date.setUTCMinutes(instance_date.getUTCMinutes() +instance_date.getTimezoneOffset());
1755
		}
1756
		if (typeof content != 'undefined' && content.id != null &&
1757
			typeof content.recur_type != 'undefined' && content.recur_type != null && content.recur_type != 0
1758
		)
1759
		{
1760
			if (content.start != start_date ||
1761
				content.whole_day != is_whole_day ||
1762
				(duration && ''+content.duration != duration ||
1763
				// End date might ignore seconds, and be 59 seconds off for all day events
1764
				!duration && Math.abs(new Date(end_date) - new Date(content.end)) > 60000)
1765
			)
1766
			{
1767
				et2_calendar_event.series_split_prompt(
1768
					content, instance_date, function(_button_id)
1769
					{
1770
						if (_button_id == et2_dialog.OK_BUTTON)
1771
						{
1772
							that.et2._inst.submit(button);
1773
1774
						}
1775
					}
1776
				);
1777
			}
1778
			else
1779
			{
1780
				return true;
1781
			}
1782
		}
1783
		else
1784
		{
1785
			return true;
1786
		}
1787
	},
1788
1789
	/**
1790
	 * Sidebox merge
1791
	 *
1792
	 * Manage the state and pass the request to the correct place.  Since the nextmatch
1793
	 * and the sidebox have different ideas of the 'current' timespan (sidebox
1794
	 * always has a start and end date) we need to call merge on the nextmatch
1795
	 * if the current view is listview, so the user gets the results they expect.
1796
	 *
1797
	 * @param {Event} event UI event
1798
	 * @param {et2_widget} widget Should be the merge selectbox
1799
	 */
1800
	sidebox_merge: function(event, widget)
1801
	{
1802
		if(!widget || !widget.getValue()) return false;
1803
1804
		if(this.state.view == 'listview')
1805
		{
1806
			// If user is looking at the list, pretend they used the context
1807
			// menu and process it through the nextmatch
1808
			var nm = etemplate2.getById('calendar-list').widgetContainer.getWidgetById('nm') || false;
1809
			var selected = nm ? nm.controller._objectManager.getSelectedLinks() : [];
1810
			var action = nm.controller._actionManager.getActionById('document_'+widget.getValue());
1811
			if(nm && (!selected || !selected.length))
1812
			{
1813
				nm.controller._selectionMgr.selectAll(true);
1814
			}
1815
			if(action && selected)
1816
			{
1817
				action.execute(selected);
1818
			}
1819
		}
1820
		else
1821
		{
1822
			// Set the hidden inputs to the current time span & submit
1823
			widget.getRoot().getWidgetById('first').set_value(app.calendar.state.first);
1824
			widget.getRoot().getWidgetById('last').set_value(app.calendar.state.last);
1825
			widget.getInstanceManager().postSubmit();
1826
		}
1827
		window.setTimeout(function() {widget.set_value('');},100);
1828
1829
		return false;
1830
	},
1831
1832
	/**
1833
	 * Method to set state for JSON requests (jdots ajax_exec or et2 submits can NOT use egw.js script tag)
1834
	 *
1835
	 * @param {object} _state
1836
	 */
1837
	set_state: function(_state)
1838
	{
1839
		if (typeof _state == 'object')
1840
		{
1841
			// If everything is loaded, handle the changes
1842
			if(this.sidebox_et2 !== null)
1843
			{
1844
				this.update_state(_state);
1845
			}
1846
			else
1847
			{
1848
				// Things aren't loaded yet, just set it
1849
				this.state = _state;
1850
			}
1851
		}
1852
	},
1853
1854
	/**
1855
	 * Change only part of the current state.
1856
	 *
1857
	 * The passed state options (filters) are merged with the current state, so
1858
	 * this is the one that should be used for most calls, as setState() requires
1859
	 * the complete state.
1860
	 *
1861
	 * @param {Object} _set New settings
1862
	 */
1863
	update_state: function update_state(_set)
1864
	{
1865
		// Make sure we're running in top window
1866
		if(window !== window.top && window.top.app.calendar)
1867
		{
1868
			return window.top.app.calendar.update_state(_set);
1869
		}
1870
		if(this.state_update_in_progress) return;
1871
1872
		var changed = [];
1873
		var new_state = jQuery.extend({}, this.state);
1874
		if (typeof _set === 'object')
1875
		{
1876
			for(var s in _set)
1877
			{
1878
				if (new_state[s] !== _set[s] && (typeof new_state[s] == 'string' || typeof new_state[s] !== 'string' && new_state[s]+'' !== _set[s]+''))
1879
				{
1880
					changed.push(s + ': ' + new_state[s] + ' -> ' + _set[s]);
1881
					new_state[s] = _set[s];
1882
				}
1883
			}
1884
		}
1885
		if(changed.length && !this.state_update_in_progress)
1886
		{
1887
			// This activates calendar app if you call setState from a different app
1888
			// such as home.  If we change state while not active, sizing is wrong.
1889
			if(typeof framework !== 'undefined' && framework.applications.calendar && framework.applications.calendar.hasSideboxMenuContent)
1890
			{
1891
				framework.setActiveApp(framework.applications.calendar);
1892
			}
1893
1894
			console.log('Calendar state changed',changed.join("\n"));
0 ignored issues
show
Debugging Code introduced by
console.log looks like debug code. Are you sure you do not want to remove it?
Loading history...
1895
			// Log
1896
			this.egw.debug('navigation','Calendar state changed', changed.join("\n"));
1897
			this.setState({state: new_state});
1898
		}
1899
	},
1900
1901
	/**
1902
	 * Return state object defining current view
1903
	 *
1904
	 * Called by favorites to query current state.
1905
	 *
1906
	 * @return {object} description
1907
	 */
1908
	getState: function getState()
1909
	{
1910
		var state = jQuery.extend({},this.state);
1911
1912
		if (!state)
1913
		{
1914
			var egw_script_tag = document.getElementById('egw_script_id');
1915
			state = egw_script_tag.getAttribute('data-calendar-state');
1916
			state = state ? JSON.parse(state) : {};
1917
		}
1918
1919
		// Don't store current user in state to allow admins to create favourites for all
1920
		// Should make no difference for normal users.
1921
		if(state.owner == egw.user('account_id'))
1922
		{
1923
			// 0 is always the current user, so if an admin creates a default favorite,
1924
			// it will work for other users too.
1925
			state.owner = 0;
1926
		}
1927
1928
		// Keywords are only for list view
1929
		if(state.view == 'listview')
1930
		{
1931
			var listview = app.classes.calendar.views.listview.etemplates[0] &&
1932
				app.classes.calendar.views.listview.etemplates[0].widgetContainer &&
1933
				app.classes.calendar.views.listview.etemplates[0].widgetContainer.getWidgetById('nm');
1934
			if(listview && listview.activeFilters && listview.activeFilters.search)
1935
			{
1936
				state.keywords = listview.activeFilters.search;
1937
			}
1938
		}
1939
1940
		// Don't store date or first and last
1941
		delete state.date;
1942
		delete state.first;
1943
		delete state.last;
1944
		delete state.startdate;
1945
		delete state.enddate;
1946
		delete state.start_date;
1947
		delete state.end_date;
1948
1949
		return state;
1950
	},
1951
1952
	/**
1953
	 * Set a state previously returned by getState
1954
	 *
1955
	 * Called by favorites to set a state saved as favorite.
1956
	 *
1957
	 * @param {object} state containing "name" attribute to be used as "favorite" GET parameter to a nextmatch
1958
	 */
1959
	setState: function setState(state)
1960
	{
1961
		// State should be an object, not a string, but we'll parse
1962
		if(typeof state == "string")
1963
		{
1964
			if(state.indexOf('{') != -1 || state =='null')
1965
			{
1966
				state = JSON.parse(state);
1967
			}
1968
		}
1969
		if(typeof state.state !== 'object' || !state.state.view)
1970
		{
1971
			state.state = {view: 'week'};
1972
		}
1973
		// States with no name (favorites other than No filters) default to
1974
		// today.  Applying a favorite should keep the current date.
1975
		if(!state.state.date)
1976
		{
1977
			state.state.date = state.name ? this.state.date : new Date();
1978
		}
1979
		if(typeof state.state.weekend == 'undefined')
1980
		{
1981
			state.state.weekend = true;
1982
		}
1983
1984
		// Hide other views
1985
		var view = app.classes.calendar.views[state.state.view];
1986
		for(var _view in app.classes.calendar.views)
1987
		{
1988
			if(state.state.view != _view && app.classes.calendar.views[_view])
1989
			{
1990
				for(var i = 0; i < app.classes.calendar.views[_view].etemplates.length; i++)
1991
				{
1992
					if(typeof app.classes.calendar.views[_view].etemplates[i] !== 'string' &&
1993
						view.etemplates.indexOf(app.classes.calendar.views[_view].etemplates[i]) == -1)
1994
					{
1995
						jQuery(app.classes.calendar.views[_view].etemplates[i].DOMContainer).hide();
1996
					}
1997
				}
1998
			}
1999
		}
2000
		if(this.sidebox_et2)
2001
		{
2002
			jQuery(this.sidebox_et2.getInstanceManager().DOMContainer).hide();
2003
		}
2004
2005
		// Check for valid cache
2006
		var cachable_changes = ['date','weekend','view','days','planner_view','sortby'];
2007
		var keys = jQuery.unique(Object.keys(this.state).concat(Object.keys(state.state)));
2008
		for(var i = 0; i < keys.length; i++)
2009
		{
2010
			var s = keys[i];
2011
			if (this.state[s] !== state.state[s])
2012
			{
2013
				if(cachable_changes.indexOf(s) === -1)
2014
				{
2015
					// Expire daywise cache
2016
					var daywise = egw.dataKnownUIDs(app.classes.calendar.DAYWISE_CACHE_ID);
2017
2018
					// Can't delete from here, as that would disconnect the existing widgets listening
2019
					for(var i = 0; i < daywise.length; i++)
2020
					{
2021
						egw.dataStoreUID(app.classes.calendar.DAYWISE_CACHE_ID + '::' + daywise[i],null);
2022
					}
2023
					break;
2024
				}
2025
			}
2026
		}
2027
2028
		// Check for a supported client-side view
2029
		if(app.classes.calendar.views[state.state.view] &&
2030
			// Check that the view is instanciated
2031
			typeof app.classes.calendar.views[state.state.view].etemplates[0] !== 'string' && app.classes.calendar.views[state.state.view].etemplates[0].widgetContainer
2032
		)
2033
		{
2034
			// Doing an update - this includes the selected view, and the sidebox
2035
			// We set a flag to ignore changes from the sidebox which would
2036
			// cause infinite loops.
2037
			this.state_update_in_progress = true;
2038
2039
			// Sanitize owner so it's always an array
2040
			if(state.state.owner === null || !state.state.owner ||
2041
				(typeof state.state.owner.length != 'undefined' && state.state.owner.length == 0)
2042
			)
2043
			{
2044
				state.state.owner = undefined;
2045
			}
2046
			switch(typeof state.state.owner)
0 ignored issues
show
Coding Style introduced by
As per coding-style, switch statements should have a default case.
Loading history...
2047
			{
2048
				case 'undefined':
2049
					state.state.owner = [this.egw.user('account_id')];
2050
					break;
2051
				case 'string':
2052
					state.state.owner = state.state.owner.split(',');
2053
					break;
2054
				case 'number':
2055
					state.state.owner = [state.state.owner];
2056
					break;
2057
				case 'object':
2058
					// An array-like Object or an Array?
2059
					if(!state.state.owner.filter)
2060
					{
2061
						state.state.owner = jQuery.map(state.state.owner, function(owner) {return owner;});
2062
					}
2063
			}
2064
			// Remove duplicates
2065
			state.state.owner = state.state.owner.filter(function(value, index, self) {
2066
				return self.indexOf(value) === index;
2067
			});
2068
			// Make sure they're all strings
2069
			state.state.owner = state.state.owner.map(function(owner) { return ''+owner;});
2070
			// Keep sort order
2071
			if(typeof this.state.owner === 'object')
2072
			{
2073
				var owner = [];
2074
				this.state.owner.forEach(function(key) {
2075
					var found = false;
2076
					state.state.owner = state.state.owner.filter(function(item) {
2077
						if(!found && item == key) {
2078
							owner.push(item);
2079
							found = true;
2080
							return false;
2081
						} else
2082
							return true;
2083
					});
2084
				});
2085
				// Add in any new owners
2086
				state.state.owner = owner.concat(state.state.owner);
2087
			}
2088
			if (state.state.owner.indexOf('0') >= 0)
2089
			{
2090
				state.state.owner[state.state.owner.indexOf('0')] = this.egw.user('account_id');
2091
			}
2092
2093
			// Show the correct number of grids
2094
			var grid_count = 0;
2095
			switch(state.state.view)
0 ignored issues
show
Coding Style introduced by
As per coding-style, switch statements should have a default case.
Loading history...
2096
			{
2097
				case 'day':
2098
					grid_count = 1;
2099
					break;
2100
				case 'day4':
2101
				case 'week':
2102
					grid_count = state.state.owner.length >= parseInt(this.egw.preference('week_consolidate','calendar')) ? 1 : state.state.owner.length;
2103
					break;
2104
				case 'weekN':
2105
					grid_count = parseInt(this.egw.preference('multiple_weeks','calendar')) || 3;
2106
					break;
2107
				// Month is calculated individually for the month
2108
			}
2109
2110
			var grid = view.etemplates[0].widgetContainer.getWidgetById('view');
2111
2112
			// Show the templates for the current view
2113
			// Needs to be visible while updating so sizing works
2114
			for(var i = 0; i < view.etemplates.length; i++)
2115
			{
2116
				jQuery(view.etemplates[i].DOMContainer).show();
2117
			}
2118
2119
			/*
2120
			If the count is different, we need to have the correct number
2121
			If the count is > 1, it's either because there are multiple date spans (weekN, month) and we need the correct span
2122
			per row, or there are multiple owners and we need the correct owner per row.
2123
			*/
2124
			if(grid)
2125
			{
2126
				// Show loading div to hide redrawing
2127
				egw.loading_prompt(
2128
					this.appname,true,egw.lang('please wait...'),
2129
					typeof framework !== 'undefined' ? framework.applications.calendar.tab.contentDiv : false,
2130
					egwIsMobile()?'horizental':'spinner'
2131
				);
2132
2133
				var loading = false;
2134
2135
2136
				var value = [];
2137
				state.state.first = view.start_date(state.state).toJSON();
2138
				// We'll modify this one, so it needs to be a new object
2139
				var date = new Date(state.state.first);
2140
2141
				// Hide all but the first day header
2142
				jQuery(grid.getDOMNode()).toggleClass(
2143
					'hideDayColHeader',
2144
					state.state.view == 'week' || state.state.view == 'day4'
2145
				);
2146
2147
				// Determine the different end date & varying values
2148
				switch(state.state.view)
2149
				{
2150
					case 'month':
2151
						var end = state.state.last = view.end_date(state.state);
2152
						grid_count = Math.ceil((end - date) / (1000 * 60 * 60 * 24) / 7);
2153
						// fall through
2154
					case 'weekN':
2155
						for(var week = 0; week < grid_count; week++)
2156
						{
2157
							var val = {
2158
								id: app.classes.calendar._daywise_cache_id(date,state.state.owner),
2159
								start_date: date.toJSON(),
2160
								end_date: new Date(date.toJSON()),
2161
								owner: state.state.owner
2162
							};
2163
							val.end_date.setUTCHours(24*7-1);
2164
							val.end_date.setUTCMinutes(59);
2165
							val.end_date.setUTCSeconds(59);
2166
							val.end_date = val.end_date.toJSON();
2167
							value.push(val);
2168
							date.setUTCHours(24*7);
2169
						}
2170
						state.state.last=val.end_date;
2171
						break;
2172
					case 'day':
2173
						var end = state.state.last = view.end_date(state.state).toJSON();
2174
							value.push({
2175
							id: app.classes.calendar._daywise_cache_id(date,state.state.owner),
2176
								start_date: state.state.first,
2177
								end_date: state.state.last,
2178
								owner: view.owner(state.state)
2179
							});
2180
						break;
2181
					default:
2182
						var end = state.state.last = view.end_date(state.state).toJSON();
2183
						for(var owner = 0; owner < grid_count && owner < state.state.owner.length; owner++)
2184
						{
2185
							var _owner = grid_count > 1 ? state.state.owner[owner] || 0 : state.state.owner;
2186
							value.push({
2187
								id: app.classes.calendar._daywise_cache_id(date,_owner),
2188
								start_date: date,
2189
								end_date: end,
2190
								owner: _owner
2191
							});
2192
						}
2193
						break;
2194
				}
2195
				// If we have cached data for the timespan, pass it along
2196
				// Single day with multiple owners still needs owners split to satisfy
2197
				// caching keys, otherwise they'll fetch & cache consolidated
2198
				if(state.state.view == 'day' && state.state.owner.length < parseInt(this.egw.preference('day_consolidate','calendar')))
2199
				{
2200
					var day_value = [];
2201
					for(var i = 0; i < state.state.owner.length; i++)
2202
					{
2203
						day_value.push({
2204
							start_date: state.state.first,
2205
							end_date: state.state.last,
2206
							owner: state.state.owner[i]
2207
						});
2208
					}
2209
					loading = this._need_data(day_value,state.state);
2210
				}
2211
				else
2212
				{
2213
					loading = this._need_data(value,state.state);
2214
				}
2215
2216
				var row_index = 0;
2217
2218
				// Find any matching, existing rows - they can be kept
2219
				grid.iterateOver(function(widget) {
2220
					for(var i = 0; i < value.length; i++)
2221
					{
2222
						if(widget.id == value[i].id)
2223
						{
2224
							// Keep it, but move it
2225
							if(i > row_index)
2226
							{
2227
								for(var j = i-row_index; j > 0; j--)
2228
								{
2229
									// Move from the end to the start
2230
									grid._children.unshift(grid._children.pop());
2231
2232
									// Swap DOM nodes
2233
									var a = grid._children[0].getDOMNode().parentNode.parentNode;
2234
									var a_scroll = jQuery('.calendar_calTimeGridScroll',a).scrollTop();
2235
									var b = grid._children[1].getDOMNode().parentNode.parentNode;
2236
									a.parentNode.insertBefore(a,b);
2237
2238
									// Moving nodes changes scrolling, so set it back
2239
									var a_scroll = jQuery('.calendar_calTimeGridScroll',a).scrollTop(a_scroll);
2240
								}
2241
							}
2242
							else if (row_index > i)
2243
							{
2244
								// Swap DOM nodes
2245
								var a = grid._children[row_index].getDOMNode().parentNode.parentNode;
2246
								var a_scroll = jQuery('.calendar_calTimeGridScroll',a).scrollTop();
2247
								var b = grid._children[i].getDOMNode().parentNode.parentNode;
2248
2249
								// Simple scroll forward, put top on the bottom
2250
								// This makes it faster if they scroll back next
2251
								if(i==0 && row_index == 1)
2252
								{
2253
									jQuery(b).appendTo(b.parentNode);
2254
									grid._children.push(grid._children.shift());
2255
								}
2256
								else
2257
								{
2258
									grid._children.splice(i,0,widget);
2259
									grid._children.splice(row_index+1,1);
2260
									a.parentNode.insertBefore(a,b);
2261
								}
2262
2263
								// Moving nodes changes scrolling, so set it back
2264
								var a_scroll = jQuery('.calendar_calTimeGridScroll',a).scrollTop(a_scroll);
2265
							}
2266
							break;
2267
						}
2268
					}
2269
					row_index++;
2270
				},this,et2_calendar_view);
2271
				row_index = 0;
2272
2273
				// Set rows that need it
2274
				grid.iterateOver(function(widget) {
2275
					var was_disabled = false;
2276
					if(row_index < value.length)
2277
					{
2278
						was_disabled = widget.options.disabled;
2279
						widget.set_disabled(false);
2280
					}
2281
					else
2282
					{
2283
						widget.set_disabled(true);
2284
						return;
2285
					}
2286
					if(widget.set_show_weekend)
2287
					{
2288
						widget.set_show_weekend(view.show_weekend(state.state));
2289
					}
2290
					if(widget.set_granularity)
2291
					{
2292
						if(widget.loader) widget.loader.show();
2293
						widget.set_granularity(view.granularity(state.state));
2294
					}
2295
					if(widget.id == value[row_index].id &&
2296
						widget.get_end_date().getUTCFullYear() == value[row_index].end_date.substring(0,4) &&
2297
						widget.get_end_date().getUTCMonth()+1 == value[row_index].end_date.substring(5,7) &&
2298
						widget.get_end_date().getUTCDate() == value[row_index].end_date.substring(8,10)
2299
					)
2300
					{
2301
						// Do not need to re-set this row, but we do need to re-do
2302
						// the times, as they may have changed
2303
						widget.resizeTimes();
2304
						window.setTimeout(jQuery.proxy(widget.set_header_classes, widget),0);
2305
2306
						// If disabled while the daycols were loaded, they won't load their events
2307
						for(var day = 0; was_disabled && day < widget.day_widgets.length; day++)
2308
						{
2309
							egw.dataStoreUID(
2310
									widget.day_widgets[day].registeredUID,
2311
								egw.dataGetUIDdata(widget.day_widgets[day].registeredUID).data
2312
							);
2313
						}
2314
2315
						// Hide loader
2316
						widget.loader.hide();
2317
						row_index++;
2318
						return;
2319
					}
2320
					if(widget.set_value)
2321
					{
2322
						widget.set_value(value[row_index++]);
2323
					}
2324
				},this, et2_calendar_view);
2325
			}
2326
			else if(state.state.view !== 'listview')
2327
			{
2328
				// Simple, easy case - just one widget for the selected time span. (planner)
2329
				// Update existing view's special attribute filters, defined in the view list
2330
				for(var updater in view)
2331
				{
2332
					if(typeof view[updater] === 'function')
2333
					{
2334
						var value = view[updater].call(this,state.state);
2335
						if(updater === 'start_date') state.state.first = this.date.toString(value);
2336
						if(updater === 'end_date') state.state.last = this.date.toString(value);
2337
2338
						// Set value
2339
						for(var i = 0; i < view.etemplates.length; i++)
2340
						{
2341
							view.etemplates[i].widgetContainer.iterateOver(function(widget) {
2342
								if(typeof widget['set_'+updater] === 'function')
2343
								{
2344
									widget['set_'+updater](value);
2345
								}
2346
							}, this, et2_calendar_view);
2347
						}
2348
					}
2349
				}
2350
				var value = [{start_date: state.state.first, end_date: state.state.last}];
2351
				loading = this._need_data(value,state.state);
2352
			}
2353
			// Include first & last dates in state, mostly for server side processing
2354
			if(state.state.first && state.state.first.toJSON) state.state.first = state.state.first.toJSON();
2355
			if(state.state.last && state.state.last.toJSON) state.state.last = state.state.last.toJSON();
2356
2357
			// Toggle todos
2358
			if((state.state.view == 'day' || this.state.view == 'day') && jQuery(view.etemplates[0].DOMContainer).is(':visible'))
2359
			{
2360
				if(state.state.view == 'day' && state.state.owner.length === 1 && !isNaN(state.state.owner) && state.state.owner[0] >= 0 && !egwIsMobile())
2361
				{
2362
					// Set width to 70%, otherwise if a scrollbar is needed for the view, it will conflict with the todo list
2363
					jQuery(app.classes.calendar.views.day.etemplates[0].DOMContainer).css("width","70%");
2364
					jQuery(view.etemplates[1].DOMContainer).css({"left":"70%", "height":(jQuery(framework.tabsUi.activeTab.contentDiv).height()-30)+'px'});
2365
					// TODO: Maybe some caching here
2366
					this.egw.jsonq('calendar_uiviews::ajax_get_todos', [state.state.date, state.state.owner[0]], function(data) {
2367
						this.getWidgetById('label').set_value(data.label||'');
2368
						this.getWidgetById('todos').set_value({content:data.todos||''});
2369
					},view.etemplates[1].widgetContainer);
2370
					view.etemplates[0].resize();
2371
				}
2372
				else
2373
				{
2374
					jQuery(app.classes.calendar.views.day.etemplates[1].DOMContainer).css("left","100%");
2375
					jQuery(app.classes.calendar.views.day.etemplates[1].DOMContainer).hide();
2376
					jQuery(app.classes.calendar.views.day.etemplates[0].DOMContainer).css("width","100%");
2377
					view.etemplates[0].widgetContainer.iterateOver(function(w) {
2378
						w.set_width('100%');
2379
					},this,et2_calendar_timegrid);
2380
				}
2381
			}
2382
			else if(jQuery(view.etemplates[0].DOMContainer).is(':visible'))
2383
			{
2384
				jQuery(view.etemplates[0].DOMContainer).css("width","");
2385
				view.etemplates[0].widgetContainer.iterateOver(function(w) {
2386
					w.set_width('100%');
2387
				},this,et2_calendar_timegrid);
2388
			}
2389
2390
			// List view (nextmatch) has slightly different fields
2391
			if(state.state.view === 'listview')
2392
			{
2393
				state.state.startdate = state.state.date;
2394
				if(state.state.startdate.toJSON)
2395
				{
2396
					state.state.startdate = state.state.startdate.toJSON();
2397
				}
2398
2399
				if(state.state.end_date)
2400
				{
2401
					state.state.enddate = state.state.end_date;
2402
				}
2403
				if(state.state.enddate && state.state.enddate.toJSON)
2404
				{
2405
					state.state.enddate = state.state.enddate.toJSON();
2406
				}
2407
				state.state.col_filter = {participant: state.state.owner};
2408
				state.state.search = state.state.keywords ? state.state.keywords : state.state.search;
2409
2410
2411
				var nm = view.etemplates[0].widgetContainer.getWidgetById('nm');
2412
2413
				// 'Custom' filter needs an end date
2414
				if(nm.activeFilters.filter === 'custom' && !state.state.end_date)
2415
				{
2416
					state.state.enddate = state.state.last;
2417
				}
2418
				if(state.state.enddate && state.state.startdate && state.state.startdate > state.state.enddate)
2419
				{
2420
					state.state.enddate = state.state.startdate;
2421
				}
2422
				nm.applyFilters(state.state);
2423
2424
				// Try to keep last value up to date with what's in nextmatch
2425
				if(nm.activeFilters.enddate)
2426
				{
2427
					this.state.last = nm.activeFilters.enddate;
2428
				}
2429
				// Updates the display of start & end date
2430
				this.filter_change();
2431
			}
2432
			else
2433
			{
2434
				// Turn off nextmatch's automatic stuff - it won't work while it
2435
				// is hidden, and can cause an infinite loop as it tries to layout.
2436
				// (It will automatically re-start when shown)
2437
				try
2438
				{
2439
					var nm = app.classes.calendar.views.listview.etemplates[0].widgetContainer.getWidgetById('nm');
2440
					nm.controller._grid.doInvalidate = false;
2441
				} catch (e) {}
0 ignored issues
show
Coding Style Comprehensibility Best Practice introduced by
Empty catch clauses should be used with caution; consider adding a comment why this is needed.
Loading history...
2442
				// Other views do not search
2443
				delete state.state.keywords;
2444
			}
2445
			this.state = jQuery.extend({},state.state);
2446
2447
			/* Update re-orderable calendars */
2448
			this._sortable();
2449
2450
			/* Update sidebox widgets to show current value*/
2451
			if(this.sidebox_hooked_templates.length)
2452
			{
2453
				for(var j = 0; j < this.sidebox_hooked_templates.length; j++)
2454
				{
2455
					var sidebox = this.sidebox_hooked_templates[j];
2456
					// Remove any destroyed or not valid templates
2457
					if(!sidebox.getInstanceManager || !sidebox.getInstanceManager())
2458
					{
2459
						this.sidebox_hooked_templates.splice(j,1,0);
2460
						continue;
2461
					}
2462
					sidebox.iterateOver(function(widget) {
2463
						if(widget.id == 'view')
2464
						{
2465
							// View widget has a list of state settings, which require special handling
2466
							for(var i = 0; i < widget.options.select_options.length; i++)
2467
							{
2468
								var option_state = JSON.parse(widget.options.select_options[i].value) || [];
2469
								var match = true;
2470
								for(var os_key in option_state)
2471
								{
2472
									// Sometimes an optional state variable is not yet defined (sortby, days, etc)
2473
									match = match && (option_state[os_key] == this.state[os_key] || typeof this.state[os_key] == 'undefined');
2474
								}
2475
								if(match)
2476
								{
2477
									widget.set_value(widget.options.select_options[i].value);
2478
									return;
2479
								}
2480
							}
2481
						}
2482
						else if (widget.id == 'keywords')
2483
						{
2484
							widget.set_value('');
2485
						}
2486
						else if(typeof state.state[widget.id] !== 'undefined' && state.state[widget.id] != widget.getValue())
2487
						{
2488
							// Update widget.  This may trigger an infinite loop of
2489
							// updates, so we do it after changing this.state and set a flag
2490
							try
2491
							{
2492
								widget.set_value(state.state[widget.id]);
2493
							}
2494
							catch(e)
2495
							{
2496
								widget.set_value('');
2497
							}
2498
						}
2499
						else if (widget.instanceOf(et2_inputWidget) && typeof state.state[widget.id] == 'undefined')
2500
						{
2501
							// No value, clear it
2502
							widget.set_value('');
2503
						}
2504
					},this,et2_valueWidget);
2505
				}
2506
			}
2507
2508
			// If current state matches a favorite, hightlight it
2509
			this.highlight_favorite();
2510
2511
			// Update app header
2512
			this.set_app_header(view.header(state.state));
2513
2514
			// Reset auto-refresh timer
2515
			this._set_autorefresh();
2516
2517
			// Sidebox is updated, we can clear the flag
2518
			this.state_update_in_progress = false;
2519
2520
			// Update saved state in preferences
2521
			var save = {};
2522
			for(var i = 0; i < this.states_to_save.length; i++)
2523
			{
2524
				save[this.states_to_save[i]] = this.state[this.states_to_save[i]];
2525
			}
2526
			egw.set_preference('calendar','saved_states', save);
2527
2528
			// Trigger resize to get correct sizes, as they may have sized while
2529
			// hidden
2530
			for(var i = 0; i < view.etemplates.length; i++)
2531
			{
2532
				view.etemplates[i].resize();
2533
			}
2534
2535
			// If we need to fetch data from the server, it will hide the loader
2536
			// when done but if everything is in the cache, hide from here.
2537
			if(!loading)
2538
			{
2539
				window.setTimeout(jQuery.proxy(function() {
2540
2541
					egw.loading_prompt(this.appname,false);
2542
				},this),500);
2543
			}
2544
2545
			return;
2546
		}
2547
		// old calendar state handling on server-side (incl. switching to and from listview)
2548
		var menuaction = 'calendar.calendar_uiviews.index';
2549
		if (typeof state.state != 'undefined' && (typeof state.state.view == 'undefined' || state.state.view == 'listview'))
2550
		{
2551
			if (state.name)
2552
			{
2553
				// 'blank' is the special name for no filters, send that instead of the nice translated name
2554
				state.state.favorite = jQuery.isEmptyObject(state) || jQuery.isEmptyObject(state.state||state.filter) ? 'blank' : state.name.replace(/[^A-Za-z0-9-_]/g, '_');
2555
				// set date for "No Filter" (blank) favorite to todays date
2556
				if (state.state.favorite == 'blank')
2557
					state.state.date = jQuery.datepicker.formatDate('yymmdd', new Date);
2558
			}
2559
			menuaction = 'calendar.calendar_uilist.listview';
2560
			state.state.ajax = 'true';
2561
			// check if we already use et2 / are in listview
2562
			if (this.et2 || etemplate2 && etemplate2.getByApplication('calendar'))
2563
			{
2564
				// current calendar-code can set regular calendar states only via a server-request :(
2565
				// --> check if we only need to set something which can be handeled by nm internally
2566
				// or we need a redirect
2567
				// ToDo: pass them via nm's get_rows call to server (eg. by passing state), so we dont need a redirect
2568
				var current_state = this.getState();
2569
				var need_redirect = false;
2570
				for(var attr in current_state)
2571
				{
2572
					switch(attr)
0 ignored issues
show
Coding Style introduced by
As per coding-style, switch statements should have a default case.
Loading history...
2573
					{
2574
						case 'cat_id':
2575
						case 'owner':
2576
						case 'filter':
2577
							if (state.state[attr] != current_state[attr])
2578
							{
2579
								need_redirect = true;
2580
								// reset of attributes managed on server-side
2581
								if (state.state.favorite === 'blank')
2582
								{
2583
									switch(attr)
2584
									{
2585
										case 'cat_id':
2586
											state.state.cat_id = 0;
2587
											break;
2588
										case 'owner':
2589
											state.state.owner = egw.user('account_id');
2590
											break;
2591
										case 'filter':
2592
											state.state.filter = 'default';
2593
											break;
2594
									}
2595
								}
2596
								break;
2597
							}
2598
							break;
2599
2600
						case 'view':
2601
							// "No filter" (blank) favorite: if not in listview --> stay in that view
2602
							if (state.state.favorite === 'blank' && current_state.view != 'listview')
2603
							{
2604
								menuaction = 'calendar.calendar_uiviews.index';
2605
								delete state.state.ajax;
2606
								need_redirect = true;
2607
							}
2608
					}
2609
				}
2610
				if (!need_redirect)
2611
				{
2612
					return this._super.apply(this, [state]);
2613
				}
2614
			}
2615
		}
2616
		// setting internal state now, that linkHandler does not intercept switching from listview to any old view
2617
		this.state = jQuery.extend({},state.state);
2618
		if(this.sidebox_et2)
2619
		{
2620
			jQuery(this.sidebox_et2.getInstanceManager().DOMContainer).show();
2621
		}
2622
2623
		var query = jQuery.extend({menuaction: menuaction},state.state||{});
2624
2625
		// prepend an owner 0, to reset all owners and not just set given resource type
2626
		if(typeof query.owner != 'undefined')
2627
		{
2628
			query.owner = '0,'+ (typeof query.owner == 'object' ? query.owner.join(',') : (''+query.owner).replace('0,',''));
2629
		}
2630
2631
		this.egw.open_link(this.egw.link('/index.php',query), 'calendar');
2632
2633
		// Stop the normal bubbling if this is called on click
2634
		return false;
2635
	},
2636
2637
	/**
2638
	 * Check to see if any of the selected is an event widget
2639
	 * Used to separate grid actions from event actions
2640
	 *
2641
	 * @param {egwAction} _action
2642
	 * @param {egwActioObject[]} _selected
2643
	 * @returns {boolean} Is any of the selected an event widget
2644
	 */
2645
	is_event: function(_action, _selected)
2646
	{
2647
		var is_widget = false;
2648
		for(var i = 0; i < _selected.length; i++)
2649
		{
2650
			if(_selected[i].iface.getWidget() && _selected[i].iface.getWidget().instanceOf(et2_calendar_event))
2651
			{
2652
				is_widget = true;
2653
			}
2654
2655
			// Also check classes, usually indicating permission
2656
			if(_action.data && _action.data.enableClass)
2657
			{
2658
				is_widget = is_widget && (jQuery( _selected[i].iface.getDOMNode()).hasClass(_action.data.enableClass));
2659
			}
2660
			if(_action.data && _action.data.disableClass)
2661
			{
2662
				is_widget = is_widget && !(jQuery( _selected[i].iface.getDOMNode()).hasClass(_action.data.disableClass));
2663
			}
2664
2665
		}
2666
		return is_widget;
2667
	},
2668
2669
	/**
2670
	 * Enable/Disable custom Date-time for set Alarm
2671
	 *
2672
	 * @param {egw object} _egw
2673
	 * @param {widget object} _widget new_alarm[options] selectbox
2674
	 */
2675
	alarm_custom_date: function (_egw,_widget)
2676
	{
2677
		var alarm_date = this.et2.getWidgetById('new_alarm[date]');
2678
		var alarm_options = _widget || this.et2.getWidgetById('new_alarm[options]');
2679
		var start = this.et2.getWidgetById('start');
2680
2681
		if (alarm_date && alarm_options
2682
					&& start)
2683
		{
2684
			if (alarm_options.get_value() != '0')
2685
			{
2686
				alarm_date.set_class('calendar_alarm_date_display');
2687
			}
2688
			else
2689
			{
2690
				alarm_date.set_class('');
2691
			}
2692
			var startDate = typeof start.get_value != 'undefined'?start.get_value():start.value;
2693
			if (startDate)
2694
			{
2695
				var date = new Date(startDate);
2696
				date.setTime(date.getTime() - 1000 * parseInt(alarm_options.get_value()));
2697
				alarm_date.set_value(date);
2698
			}
2699
		}
2700
	},
2701
2702
	/**
2703
	 * Set alarm options based on WD/Regular event user preferences
2704
	 * Gets fired by wholeday checkbox
2705
	 *
2706
	 * @param {egw object} _egw
2707
	 * @param {widget object} _widget whole_day checkbox
2708
	 */
2709
	set_alarmOptions_WD: function (_egw,_widget)
2710
	{
2711
		var alarm = this.et2.getWidgetById('alarm');
2712
		if (!alarm) return;	// no default alarm
2713
		var content = this.et2.getArrayMgr('content').data;
2714
		var start = this.et2.getWidgetById('start');
2715
		var self= this;
2716
		var time = alarm.cells[1][0].widget;
2717
		var event = alarm.cells[1][1].widget;
2718
		// Convert a seconds of time to a translated label
2719
		var _secs_to_label = function (_secs)
2720
		{
2721
			var label='';
2722
			if (_secs <= 3600)
2723
			{
2724
				label = self.egw.lang('%1 minutes', _secs/60);
2725
			}
2726
			else if(_secs <= 86400)
2727
			{
2728
				label = self.egw.lang('%1 hours', _secs/3600);
2729
			}
2730
			return label;
2731
		};
2732
		if (typeof content['alarm'][1]['default'] == 'undefined')
2733
		{
2734
			// user deleted alarm --> nothing to do
2735
		}
2736
		else
2737
		{
2738
			var def_alarm = this.egw.preference(_widget.get_value() === "true" ?
2739
				'default-alarm-wholeday' : 'default-alarm', 'calendar');
2740
			if (!def_alarm && def_alarm !== 0)	// no alarm
2741
			{
2742
				jQuery('#calendar-edit_alarm > tbody :nth-child(1)').hide();
2743
			}
2744
			else
2745
			{
2746
				jQuery('#calendar-edit_alarm > tbody :nth-child(1)').show();
2747
				start.set_hours(0);
2748
				start.set_minutes(0);
2749
				time.set_value(start.get_value());
2750
				time.set_value('-'+(60 * def_alarm));
2751
				event.set_value(_secs_to_label(60 * def_alarm));
2752
			}
2753
		}
2754
	},
2755
2756
2757
	/**
2758
	 * Clear all calendar data from egw.data cache
2759
	 */
2760
	_clear_cache: function() {
2761
		// Full refresh, clear the caches
2762
		var events = egw.dataKnownUIDs('calendar');
2763
		for(var i = 0; i < events.length; i++)
2764
		{
2765
			egw.dataDeleteUID('calendar::' + events[i]);
2766
		}
2767
		var daywise = egw.dataKnownUIDs(app.classes.calendar.DAYWISE_CACHE_ID);
2768
		for(var i = 0; i < daywise.length; i++)
2769
		{
2770
			// Empty to clear existing widgets
2771
			egw.dataStoreUID(app.classes.calendar.DAYWISE_CACHE_ID + '::' + daywise[i], null);
2772
		}
2773
	},
2774
2775
	/**
2776
	 * Take the date range(s) in the value and decide if we need to fetch data
2777
	 * for the date ranges, or if they're already cached fill them in.
2778
	 *
2779
	 * @param {Object} value
2780
	 * @param {Object} state
2781
	 *
2782
	 * @return {boolean} Data was requested
2783
	 */
2784
	_need_data: function(value, state)
2785
	{
2786
		var need_data = false;
2787
2788
		// Determine if we're showing multiple owners seperate or consolidated
2789
		var seperate_owners = false;
2790
		var last_owner = value.length ? value[0].owner || 0 : 0;
2791
		for(var i = 0; i < value.length && !seperate_owners; i++)
2792
		{
2793
			seperate_owners = seperate_owners || (last_owner !== value[i].owner);
2794
		}
2795
2796
		for(var i = 0; i < value.length; i++)
2797
		{
2798
			var t = new Date(value[i].start_date);
2799
			var end = new Date(value[i].end_date);
2800
			do
2801
			{
2802
				// Cache is by date (and owner, if seperate)
2803
				var date = t.getUTCFullYear() + sprintf('%02d',t.getUTCMonth()+1) + sprintf('%02d',t.getUTCDate());
2804
				var cache_id = app.classes.calendar._daywise_cache_id(date, seperate_owners && value[i].owner ? value[i].owner : state.owner||false);
2805
2806
				if(egw.dataHasUID(cache_id))
2807
				{
2808
					var c = egw.dataGetUIDdata(cache_id);
2809
					if(c.data && c.data !== null)
2810
					{
2811
						// There is data, pass it along now
2812
						value[i][date] = [];
2813
						for(var j = 0; j < c.data.length; j++)
2814
						{
2815
							if(egw.dataHasUID('calendar::'+c.data[j]))
2816
							{
2817
								value[i][date].push(egw.dataGetUIDdata('calendar::'+c.data[j]).data);
2818
							}
2819
							else
2820
							{
2821
								need_data = true;
2822
							}
2823
						}
2824
					}
2825
					else
2826
					{
2827
						need_data = true;
2828
						// Assume it's empty, if there is data it will be filled later
2829
						egw.dataStoreUID(cache_id, []);
2830
					}
2831
				}
2832
				else
2833
				{
2834
					need_data = true;
2835
					// Assume it's empty, if there is data it will be filled later
2836
					egw.dataStoreUID(cache_id, []);
2837
				}
2838
				t.setUTCDate(t.getUTCDate() + 1);
2839
			}
2840
			while(t < end);
2841
2842
			// Some data is missing for the current owner, go get it
2843
			if(need_data && seperate_owners)
2844
			{
2845
				this._fetch_data(
2846
					jQuery.extend({}, state, {owner: value[i].owner, selected_owners: state.owner}),
2847
					this.sidebox_et2 ? null : this.et2.getInstanceManager()
2848
				);
2849
				need_data = false;
2850
			}
2851
		}
2852
2853
		// Some data was missing, go get it
2854
		if(need_data && !seperate_owners)
2855
		{
2856
			this._fetch_data(
2857
				state,
2858
				this.sidebox_et2 ? null : this.et2.getInstanceManager()
2859
			);
2860
		}
2861
2862
		return need_data;
2863
	},
2864
2865
	/**
2866
	 * Use the egw.data system to get data from the calendar list for the
2867
	 * selected time span.
2868
	 *
2869
	 * As long as the other filters are the same (category, owner, status) we
2870
	 * cache the data.
2871
	 *
2872
	 * @param {Object} state
2873
	 * @param {etemplate2} [instance] If the full calendar app isn't loaded
2874
	 *	(home app), pass a different instance to use it to get the data
2875
	 * @param {number} [start] Result offset.  Internal use only
2876
	 */
2877
	_fetch_data: function(state, instance, start)
2878
	{
2879
		if(!this.sidebox_et2 && !instance)
2880
		{
2881
			return;
2882
		}
2883
2884
		if(typeof start === 'undefined')
2885
		{
2886
			start = 0;
2887
		}
2888
2889
		// Category needs to be false if empty, not an empty array or string
2890
		var cat_id = state.cat_id ? state.cat_id : false;
2891
		if(cat_id && typeof cat_id.join != 'undefined')
2892
		{
2893
			if(cat_id.join('') == '') cat_id = false;
2894
		}
2895
		// Make sure cat_id reaches to server in array format
2896
		if (cat_id && typeof cat_id == 'string' && cat_id != "0") cat_id = cat_id.split(',');
2897
2898
		var query = jQuery.extend({}, {
2899
			get_rows: 'calendar.calendar_uilist.get_rows',
2900
			row_id:'row_id',
2901
			startdate:state.first ||  state.date,
2902
			enddate:state.last,
2903
			// Participant must be an array or it won't work
2904
			col_filter: {participant: (typeof state.owner == 'string' || typeof state.owner == 'number' ? [state.owner] : state.owner)},
2905
			filter:'custom', // Must be custom to get start & end dates
2906
			status_filter: state.status_filter,
2907
			cat_id: cat_id,
2908
			csv_export: false,
2909
			selected_owners: state.selected_owners
2910
		});
2911
		// Show ajax loader
2912
		if(typeof framework !== 'undefined')
2913
		{
2914
			framework.applications.calendar.sidemenuEntry.showAjaxLoader();
2915
		}
2916
2917
		if(state.view === 'planner' && state.sortby === 'user')
2918
		{
2919
			query.order = 'participants';
2920
		}
2921
		else if (state.view === 'planner' && state.sortby === 'category')
2922
		{
2923
			query.order = 'categories';
2924
		}
2925
2926
		// Already in progress?
2927
		var query_string = JSON.stringify(query);
2928
		if(this._queries_in_progress.indexOf(query_string) != -1)
2929
		{
2930
			return;
2931
		}
2932
		this._queries_in_progress.push(query_string);
2933
2934
		this.egw.dataFetch(
2935
			instance ? instance.etemplate_exec_id :
2936
				this.sidebox_et2.getInstanceManager().etemplate_exec_id,
2937
			{start: start, num_rows:400},
2938
			query,
2939
			this.id,
2940
			function calendar_handleResponse(data) {
2941
				var idx = this._queries_in_progress.indexOf(query_string);
2942
				if(idx >= 0)
2943
				{
2944
					this._queries_in_progress.splice(idx,1);
2945
				}
2946
				//console.log(data);
2947
2948
				// Look for any updated select options
2949
				if(data.rows && data.rows.sel_options && this.sidebox_et2)
2950
				{
2951
					for(var field in data.rows.sel_options)
2952
					{
2953
						var widget = this.sidebox_et2.getWidgetById(field);
2954
						if(widget && widget.set_select_options)
2955
						{
2956
							// Merge in new, update label of existing
2957
							for(var i in data.rows.sel_options[field])
2958
							{
2959
								var found = false;
2960
								var option = data.rows.sel_options[field][i];
2961
								for(var j in widget.options.select_options)
2962
								{
2963
									if(option.value == widget.options.select_options[j].value)
2964
									{
2965
										widget.options.select_options[j].label = option.label;
2966
										found = true;
2967
										break;
2968
									}
2969
								}
2970
								if(!found)
2971
								{
2972
									if(!widget.options.select_options.push)
2973
									{
2974
										widget.options.select_options = [];
2975
									}
2976
									widget.options.select_options.push(option);
2977
								}
2978
							}
2979
							var in_progress = app.calendar.state_update_in_progress;
2980
							app.calendar.state_update_in_progress = true;
2981
							widget.set_select_options(widget.options.select_options);
2982
							widget.set_value(widget.getValue());
2983
2984
							// If updating owner, update listview participants as well
2985
							// This lets us _add_ to the options, normal nm behaviour will replace.
2986
							if(field == 'owner')
2987
							{
2988
								try {
2989
									var participant = app.classes.calendar.views.listview.etemplates[0].widgetContainer.getWidgetById('nm').getWidgetById('participant');
2990
									if(participant)
2991
									{
2992
										participant.options.select_options = widget.options.select_options;
2993
										participant.set_select_options(widget.options.select_options);
2994
									}
2995
								} catch(e) {debugger;}
0 ignored issues
show
Debugging Code introduced by
debugger looks like debug code. Are you sure you do not want to remove it?
Loading history...
2996
							}
2997
2998
							app.calendar.state_update_in_progress = in_progress;
2999
						}
3000
					}
3001
				}
3002
3003
				if(data.order && data.total)
3004
				{
3005
					this._update_events(state, data.order);
3006
				}
3007
3008
				// More rows?
3009
				if(data.order.length + start < data.total)
3010
				{
3011
					// Wait a bit, let UI do something.
3012
					window.setTimeout( function() {
3013
						app.calendar._fetch_data(state, instance, start + data.order.length);
3014
					}, 100);
3015
				}
3016
				// Hide AJAX loader
3017
				else if(typeof framework !== 'undefined')
3018
				{
3019
					framework.applications.calendar.sidemenuEntry.hideAjaxLoader();
3020
					egw.loading_prompt('calendar',false)
3021
3022
				}
3023
			}, this,null
3024
		);
3025
	},
3026
3027
	/**
3028
	 * We have a list of calendar UIDs of events that need updating.
3029
	 *
3030
	 * The event data should already be in the egw.data cache, we just need to
3031
	 * figure out where they need to go, and update the needed parent objects.
3032
	 *
3033
	 * Already existing events will have already been updated by egw.data
3034
	 * callbacks.
3035
	 *
3036
	 * @param {Object} state Current state for update, used to determine what to update
3037
	 * @param data
3038
	 */
3039
	_update_events: function(state, data) {
3040
		var updated_days = {};
3041
3042
		// Events can span for longer than we are showing
3043
		var first = new Date(state.first);
3044
		var last = new Date(state.last);
3045
		var bounds = {
3046
			first: ''+first.getUTCFullYear() + sprintf('%02d',first.getUTCMonth()+1) + sprintf('%02d',first.getUTCDate()),
3047
			last: ''+last.getUTCFullYear() + sprintf('%02d',last.getUTCMonth()+1) + sprintf('%02d',last.getUTCDate())
3048
		};
3049
		// Seperate owners, or consolidated?
3050
		var multiple_owner = typeof state.owner != 'string' &&
3051
			state.owner.length > 1 &&
3052
			(state.view == 'day' && state.owner.length < parseInt(this.egw.preference('day_consolidate','calendar')) ||
3053
			['week','day4'].indexOf(state.view) !== -1 && state.owner.length < parseInt(this.egw.preference('week_consolidate','calendar')));
3054
3055
3056
		for(var i = 0; i < data.length; i++)
3057
		{
3058
			var record = this.egw.dataGetUIDdata(data[i]);
3059
			if(record && record.data)
3060
			{
3061
				if(typeof updated_days[record.data.date] === 'undefined')
3062
				{
3063
					// Check to make sure it's in range first, record.data.date is start date
3064
					// and could be before our start
3065
					if(record.data.date >= bounds.first && record.data.date <= bounds.last)
3066
					{
3067
						updated_days[record.data.date] = [];
3068
					}
3069
				}
3070
				if(typeof updated_days[record.data.date] != 'undefined')
3071
				{
3072
					// Copy, to avoid unwanted changes by reference
3073
					updated_days[record.data.date].push(record.data.row_id);
3074
				}
3075
3076
				// Check for multi-day events listed once
3077
				// Date must stay a string or we might cause problems with nextmatch
3078
				var dates = {
3079
					start: typeof record.data.start === 'string' ? record.data.start : record.data.start.toJSON(),
3080
					end: typeof record.data.end === 'string' ? record.data.end : record.data.end.toJSON()
3081
				};
3082
				if(dates.start.substr(0,10) !== dates.end.substr(0,10) &&
3083
						// Avoid events ending at midnight having a 0 length event the next day
3084
						dates.end.substr(11,8) !== '00:00:00')
3085
				{
3086
					var end = new Date(Math.min(new Date(record.data.end), new Date(state.last)));
3087
					end.setUTCHours(23);
3088
					end.setUTCMinutes(59);
3089
					end.setUTCSeconds(59);
3090
					var t = new Date(Math.max(new Date(record.data.start), new Date(state.first)));
3091
3092
					do
3093
					{
3094
						var expanded_date = ''+t.getUTCFullYear() + sprintf('%02d',t.getUTCMonth()+1) + sprintf('%02d',t.getUTCDate());
3095
						if(typeof(updated_days[expanded_date]) === 'undefined')
3096
						{
3097
							// Check to make sure it's in range first, expanded_date could be after our end
3098
							if(expanded_date >= bounds.first && expanded_date <= bounds.last)
3099
							{
3100
								updated_days[expanded_date] = [];
3101
							}
3102
						}
3103
						if(record.data.date !== expanded_date && typeof updated_days[expanded_date] !== 'undefined')
3104
						{
3105
							// Copy, to avoid unwanted changes by reference
3106
							updated_days[expanded_date].push(record.data.row_id);
3107
						}
3108
						t.setUTCDate(t.getUTCDate() + 1);
3109
					}
3110
					while(end >= t)
3111
				}
3112
			}
3113
		}
3114
3115
		// Now we know which days changed, so we pass it on
3116
		for(var day in updated_days)
3117
		{
3118
			// Might be split by user, so we have to check that too
3119
			for(var i = 0; i < (typeof state.owner == 'object' ? state.owner.length : 1); i++)
3120
			{
3121
				var owner = multiple_owner ? state.owner[i] : state.owner;
3122
				var cache_id = app.classes.calendar._daywise_cache_id(day, owner);
3123
				if(egw.dataHasUID(cache_id))
3124
				{
3125
					// Don't lose any existing data, just append
3126
					var c = egw.dataGetUIDdata(cache_id);
3127
					if(c.data && c.data !== null)
3128
					{
3129
						// Avoid duplicates
3130
						var data = c.data.concat(updated_days[day]).filter(function(value, index, self) {
3131
							return self.indexOf(value) === index;
3132
						});
3133
						this.egw.dataStoreUID(cache_id,data);
3134
					}
3135
				}
3136
				else
3137
				{
3138
					this.egw.dataStoreUID(cache_id, updated_days[day]);
3139
				}
3140
				if(!multiple_owner) break;
3141
			}
3142
		}
3143
3144
		egw.loading_prompt(this.appname,false);
3145
	},
3146
3147
	/**
3148
	 * Some handy date calculations
3149
	 * All take either a Date object or full date with timestamp (Z)
3150
	 */
3151
	date: {
3152
		toString: function(date)
3153
		{
3154
			// Ensure consistent formatting using UTC, avoids problems with comparison
3155
			// and timezones
3156
			if(typeof date === 'string') date = new Date(date);
3157
			return date.getUTCFullYear() +'-'+
3158
				sprintf("%02d",date.getUTCMonth()+1) + '-'+
3159
				sprintf("%02d",date.getUTCDate()) + 'T'+
3160
				sprintf("%02d",date.getUTCHours()) + ':'+
3161
				sprintf("%02d",date.getUTCMinutes()) + ':'+
3162
				sprintf("%02d",date.getUTCSeconds()) + 'Z';
3163
		},
3164
3165
		/**
3166
		* Formats one or two dates (range) as long date (full monthname), optionaly with a time
3167
		*
3168
		* Take care of any timezone issues before you pass the dates in.
3169
		*
3170
		* @param {Date} first first date
3171
		* @param {Date} last =0 last date for range, or false for a single date
3172
		* @param {boolean} display_time =false should a time be displayed too
3173
		* @param {boolean} display_day =false should a day-name prefix the date, eg. monday June 20, 2006
3174
		* @return string with formatted date
3175
		*/
3176
		long_date: function(first, last, display_time, display_day)
3177
		{
3178
			if(!first) return '';
3179
			if(typeof first === 'string')
3180
			{
3181
				first = new Date(first);
3182
			}
3183
			var first_format = new Date(first.valueOf() + first.getTimezoneOffset() * 60 * 1000);
3184
3185
			if(typeof last == 'string' && last)
3186
			{
3187
				last = new Date(last);
3188
			}
3189
			if(!last || typeof last !== 'object')
3190
			{
3191
				 last = false;
3192
			}
3193
			if(last)
3194
			{
3195
				var last_format = new Date(last.valueOf() + last.getTimezoneOffset() * 60 * 1000);
3196
			}
3197
3198
			if(!display_time) display_time = false;
3199
			if(!display_day) display_day = false;
3200
3201
			var range = '';
3202
3203
			var datefmt = egw.preference('dateformat');
3204
			var timefmt = egw.preference('timeformat') === '12' ? 'h:i a' : 'H:i';
3205
3206
			var month_before_day = datefmt[0].toLowerCase() == 'm' ||
3207
				datefmt[2].toLowerCase() == 'm' && datefmt[4] == 'd';
3208
3209
			if (display_day)
3210
			{
3211
				range = jQuery.datepicker.formatDate('DD',first_format)+(datefmt[0] != 'd' ? ' ' : ', ');
3212
			}
3213
			for (var i = 0; i < 5; i += 2)
3214
			{
3215
				 switch(datefmt[i])
0 ignored issues
show
Coding Style introduced by
As per coding-style, switch statements should have a default case.
Loading history...
3216
				 {
3217
					 case 'd':
3218
						 range += first.getUTCDate()+ (datefmt[1] == '.' ? '.' : '');
3219
						 if (last && (first.getUTCMonth() != last.getUTCMonth() || first.getUTCFullYear() != last.getUTCFullYear()))
3220
						 {
3221
							 if (!month_before_day)
3222
							 {
3223
								 range += jQuery.datepicker.formatDate('MM',first_format);
3224
							 }
3225
							 if (first.getFullYear() != last.getFullYear() && datefmt[0] != 'Y')
3226
							 {
3227
								 range += (datefmt[0] != 'd' ? ', ' : ' ') + first.getFullYear();
3228
							 }
3229
							 if (display_time)
3230
							 {
3231
								 range += ' '+jQuery.datepicker.formatDate(dateTimeFormat(timefmt),first_format);
3232
							 }
3233
							 if (!last)
3234
							 {
3235
								 return range;
3236
							 }
3237
							 range += ' - ';
3238
3239
							 if (first.getFullYear() != last.getFullYear() && datefmt[0] == 'Y')
3240
							 {
3241
								 range += last.getUTCFullYear() + ', ';
3242
							 }
3243
3244
							 if (month_before_day)
3245
							 {
3246
								 range += jQuery.datepicker.formatDate('MM',last_format);
0 ignored issues
show
Bug introduced by
The variable last_format does not seem to be initialized in case last on line 3193 is false. Are you sure the function formatDate handles undefined variables?
Loading history...
3247
							 }
3248
						 }
3249
						 else
3250
						 {
3251
							 if (display_time)
3252
							 {
3253
								 range += ' '+jQuery.datepicker.formatDate(dateTimeFormat(timefmt),last_format);
3254
							 }
3255
							 if(last)
3256
							 {
3257
								 range += ' - ';
3258
							 }
3259
						 }
3260
						 if(last)
3261
						 {
3262
							 range += ' ' + last.getUTCDate() + (datefmt[1] == '.' ? '.' : '');
3263
						 }
3264
						 break;
3265
					 case 'm':
3266
					 case 'M':
3267
						 range += ' '+jQuery.datepicker.formatDate('MM',month_before_day ? first_format : last_format) + ' ';
3268
						 break;
3269
					 case 'Y':
3270
						 if (datefmt[0] != 'm')
3271
						 {
3272
							 range += ' ' + (datefmt[0] == 'Y' ? first.getUTCFullYear()+(datefmt[2] == 'd' ? ', ' : ' ') : last.getUTCFullYear()+' ');
3273
						 }
3274
						 break;
3275
				 }
3276
			}
3277
			if (display_time && last)
3278
			{
3279
				 range += ' '+jQuery.datepicker.formatDate(dateTimeFormat(timefmt),last_format);
3280
			}
3281
			if (datefmt[4] == 'Y' && datefmt[0] == 'm')
3282
			{
3283
				 range += ', ' + last.getUTCFullYear();
3284
			}
3285
			return range;
3286
		},
3287
		/**
3288
		* Calculate iso8601 week-number, which is defined for Monday as first day of week only
3289
		*
3290
		* We adjust the day, if user prefs want a different week-start-day
3291
		*
3292
		* @param {string|Date} _date
3293
		* @return string
3294
		*/
3295
		week_number: function(_date)
3296
		{
3297
			var d = new Date(_date);
3298
			var day = d.getUTCDay();
3299
3300
3301
			// if week does not start Monday and date is Sunday --> add one day
3302
			if (egw.preference('weekdaystarts','calendar') != 'Monday' && !day)
3303
			{
3304
				d.setUTCDate(d.getUTCDate() + 1);
3305
			}
3306
			// if week does start Saturday and $time is Saturday --> add two days
3307
			else if (egw.preference('weekdaystarts','calendar') == 'Saturday' && day == 6)
3308
			{
3309
				d.setUTCDate(d.getUTCDate() + 2);
3310
			}
3311
3312
			return jQuery.datepicker.iso8601Week(new Date(d.valueOf() + d.getTimezoneOffset() * 60 * 1000));
3313
		},
3314
		start_of_week: function(date)
3315
		{
3316
			var d = new Date(date);
3317
			var day = d.getUTCDay();
3318
			var diff = 0;
3319
			switch(egw.preference('weekdaystarts','calendar'))
3320
			{
3321
				case 'Saturday':
3322
					diff = day === 6 ? 0 : day === 0 ? -1 : -(day + 1);
3323
					break;
3324
				case 'Monday':
3325
					diff = day === 0 ? -6 : 1-day;
3326
					break;
3327
				case 'Sunday':
3328
				default:
3329
					diff = -day;
3330
			}
3331
			d.setUTCDate(d.getUTCDate() + diff);
3332
			return d;
3333
		},
3334
		end_of_week: function(date)
3335
		{
3336
			var d = app.calendar.date.start_of_week(date);
3337
			d.setUTCDate(d.getUTCDate() + 6);
3338
			return d;
3339
		}
3340
	},
3341
3342
	/**
3343
	 * The sidebox filters use some non-standard and not-exposed options.  They
3344
	 * are set up here.
3345
	 *
3346
	 */
3347
	_setup_sidebox_filters: function()
3348
	{
3349
		// Further date customizations
3350
		var date_widget = this.sidebox_et2.getWidgetById('date');
3351
		if(date_widget)
3352
		{
3353
			// Dynamic resize of sidebox calendar to fill sidebox
3354
			var preferred_width = jQuery('#calendar-sidebox_date .ui-datepicker-inline').outerWidth();
3355
			var font_ratio = 12 / parseFloat(jQuery('#calendar-sidebox_date .ui-datepicker-inline').css('font-size'));
3356
			var calendar_resize = function() {
3357
				try {
3358
					var percent = 1+((jQuery(date_widget.getDOMNode()).width() - preferred_width) / preferred_width);
3359
					percent *= font_ratio;
3360
					jQuery('#calendar-sidebox_date .ui-datepicker-inline')
3361
						.css('font-size',(percent*100)+'%');
3362
3363
					// Position go and today
3364
					var buttons = jQuery('#calendar-sidebox_date .ui-datepicker-header a span');
3365
					if(today.length && go_button.length)
3366
					{
3367
						go_button.position({my: 'left+8px center', at: 'right center-1',of: jQuery('#calendar-sidebox_date .ui-datepicker-year')});
3368
						today.css({
3369
							'left': (buttons.first().offset().left + buttons.last().offset().left)/2 - Math.ceil(today.outerWidth(true)/2),
3370
							'top': go_button.css('top')
3371
						});
3372
						buttons.position({my: 'center', at: 'center', of: go_button})
3373
							.css('left', '');
3374
					}
3375
				} catch (e){
3376
					// Resize didn't work
3377
				}
3378
			};
3379
3380
			var datepicker = date_widget.input_date.datepicker("option", {
3381
				showButtonPanel:	false,
3382
				onChangeMonthYear: function(year, month, inst)
3383
				{
3384
					// Update month button label
3385
					var go_button = date_widget.getRoot().getWidgetById('header_go');
3386
					if(go_button)
3387
					{
3388
						var temp_date = new Date(year, month-1, 1,0,0,0);
3389
						//temp_date.setUTCMinutes(temp_date.getUTCMinutes() + temp_date.getTimezoneOffset());
3390
						go_button.btn.attr('title',egw.lang(date('F',temp_date)));
3391
3392
						// Store current _displayed_ date in date button for clicking
3393
						temp_date.setUTCMinutes(temp_date.getUTCMinutes() - temp_date.getTimezoneOffset());
3394
						go_button.btn.attr('data-date', temp_date.toJSON());
3395
					}
3396
					window.setTimeout(calendar_resize,0);
3397
				},
3398
				// Mark holidays
3399
				beforeShowDay: function (date)
3400
				{
3401
					var holidays = et2_calendar_view.get_holidays({day_class_holiday: function() {}}, date.getFullYear());
3402
					var day_holidays = holidays[''+date.getFullYear() +
3403
						sprintf("%02d",date.getMonth()+1) +
3404
						sprintf("%02d",date.getDate())];
3405
					var css_class = '';
3406
					var tooltip = '';
3407
					if(typeof day_holidays !== 'undefined' && day_holidays.length)
3408
					{
3409
						for(var i = 0; i < day_holidays.length; i++)
3410
						{
3411
							if (typeof day_holidays[i]['birthyear'] !== 'undefined')
3412
							{
3413
								css_class +='calendar_calBirthday ';
3414
							}
3415
							else
3416
							{
3417
								css_class += 'calendar_calHoliday ';
3418
							}
3419
							tooltip += day_holidays[i]['name'] + "\n";
3420
						}
3421
					}
3422
					return [true, css_class, tooltip];
3423
				}
3424
			});
3425
3426
			// Clickable week numbers
3427
			date_widget.input_date.on('mouseenter','.ui-datepicker-week-col', function() {
3428
					jQuery(this).siblings().find('a').addClass('ui-state-hover');
3429
				})
3430
				.on('mouseleave','.ui-datepicker-week-col', function() {
3431
					jQuery(this).siblings().find('a').removeClass('ui-state-hover');
3432
				})
3433
				.on('click', '.ui-datepicker-week-col', function() {
3434
					var view = app.calendar.state.view;
3435
					var days = app.calendar.state.days;
3436
3437
					// Avoid a full state update, we just want the calendar to update
3438
					// Directly update to avoid change event from the sidebox calendar
3439
					var date = new Date(this.nextSibling.dataset.year,this.nextSibling.dataset.month,this.nextSibling.firstChild.textContent,0,0,0);
3440
					date.setUTCMinutes(date.getUTCMinutes() - date.getTimezoneOffset());
3441
					date = app.calendar.date.toString(date);
3442
3443
					// Set to week view, if in one of the views where we change view
3444
					if(app.calendar.sidebox_changes_views.indexOf(view) >= 0)
3445
					{
3446
						app.calendar.update_state({view: 'week', date: date, days: days});
3447
					}
3448
					else if (view == 'planner')
3449
					{
3450
						// Clicked a week, show just a week
3451
						app.calendar.update_state({date: date, planner_view: 'week'});
3452
					}
3453
					else if (view == 'listview')
3454
					{
3455
						app.calendar.update_state({
3456
							date: date,
3457
							end_date: app.calendar.date.toString(app.classes.calendar.views.week.end_date({date:date})),
3458
							filter: 'week'
3459
						});
3460
					}
3461
					else
3462
					{
3463
						app.calendar.update_state({date: date});
3464
					}
3465
				});
3466
3467
3468
			// Set today button
3469
			var today = jQuery('#calendar-sidebox_header_today');
3470
			today.attr('title',egw.lang('today'));
3471
3472
			// Set go button
3473
			var go_button = date_widget.getRoot().getWidgetById('header_go');
3474
			if(go_button && go_button.btn)
3475
			{
3476
				go_button = go_button.btn;
3477
				var temp_date = new Date(date_widget.get_value());
3478
				temp_date.setUTCDate(1);
3479
				temp_date.setUTCMinutes(temp_date.getUTCMinutes() + temp_date.getTimezoneOffset());
3480
3481
				go_button.attr('title', egw.lang(date('F',temp_date)));
3482
				// Store current _displayed_ date in date button for clicking
3483
				temp_date.setUTCMinutes(temp_date.getUTCMinutes() - temp_date.getTimezoneOffset());
3484
				go_button.attr('data-date', temp_date.toJSON());
3485
3486
			}
3487
		}
3488
3489
		jQuery(window).on('resize.calendar'+date_widget.dom_id,calendar_resize).trigger('resize');
0 ignored issues
show
Bug introduced by
The variable calendar_resize does not seem to be initialized in case date_widget on line 3351 is false. Are you sure the function on handles undefined variables?
Loading history...
3490
3491
		// Avoid wrapping owner icons if user has group + search
3492
		var button = jQuery('#calendar-sidebox_owner ~ span.et2_clickable');
3493
		if(button.length == 1)
3494
		{
3495
			button.parent().css('margin-right',button.outerWidth(true)+2);
3496
			button.parent().parent().css('white-space','nowrap');
3497
		}
3498
		jQuery(window).on('resize.calendar-owner', function() {
3499
			var preferred_width = jQuery('#calendar-et2_target').children().first().outerWidth()||0;
3500
			if(app.calendar && app.calendar.sidebox_et2)
3501
			{
3502
				var owner = app.calendar.sidebox_et2.getWidgetById('owner');
3503
				if(preferred_width && owner.input.hasClass("chzn-done"))
3504
				{
3505
					owner.input.next().css('width',preferred_width);
3506
				}
3507
			}
3508
		});
3509
	},
3510
3511
	/**
3512
	 * Record view templates so we can quickly switch between them.
3513
	 *
3514
	 * @param {etemplate2} _et2 etemplate2 template that was just loaded
3515
	 * @param {String} _name Name of the template
3516
	 */
3517
	_et2_view_init: function(_et2, _name)
3518
	{
3519
		var hidden = typeof this.state.view !== 'undefined';
3520
		var all_loaded = this.sidebox_et2 !== null;
3521
3522
		// Avoid home portlets using our templates, and get them right
3523
		if(_et2.uniqueId.indexOf('portlet') === 0) return;
3524
3525
		// Flag to make sure we don't hide non-view templates
3526
		var view_et2 = false;
3527
3528
		for(var view in app.classes.calendar.views)
3529
		{
3530
			var index = app.classes.calendar.views[view].etemplates.indexOf(_name);
3531
			if(index > -1)
3532
			{
3533
				view_et2 = true;
3534
				app.classes.calendar.views[view].etemplates[index] = _et2;
3535
				// If a template disappears, we want to release it
3536
				jQuery(_et2.DOMContainer).one('clear',jQuery.proxy(function() {
3537
					this.view.etemplates[this.index] = _name;
3538
				},jQuery.extend({},{view: app.classes.calendar.views[view], index: ""+index, name: _name})));
3539
3540
				if(this.state.view === view)
3541
				{
3542
					hidden = false;
3543
				}
3544
			}
3545
			app.classes.calendar.views[view].etemplates.forEach(function(et) {all_loaded = all_loaded && typeof et !== 'string';});
3546
		}
3547
3548
		// Add some extras to the nextmatch so it can keep the dates in sync with
3549
		// those in the sidebox calendar.  Care must be taken to not trigger any
3550
		// sort of refresh or update, as that may resulte in infinite loops so these
3551
		// are only used for the 'week' and 'month' filters, and we just update the
3552
		// date range
3553
		if(_name == 'calendar.list')
3554
		{
3555
			var nm = _et2.widgetContainer.getWidgetById('nm');
3556
			if(nm)
3557
			{
3558
				// Avoid unwanted refresh immediately after load
3559
				nm.controller._grid.doInvalidate = false;
3560
3561
				// Preserve pre-set search
3562
				if(nm.activeFilters.search)
3563
				{
3564
					this.state.keywords = nm.activeFilters.search;
3565
				}
3566
				// Bind to keep search up to date
3567
				jQuery(nm.getWidgetById('search').getDOMNode()).on('change', function() {
3568
					app.calendar.state.search = jQuery('input',this).val();
3569
				});
3570
				nm.set_startdate = jQuery.proxy(function(date) {
3571
					this.state.first = this.date.toString(new Date(date));
3572
				},this);
3573
				nm.set_enddate = jQuery.proxy(function(date) {
3574
					this.state.last = this.date.toString(new Date(date));
3575
				},this);
3576
			}
3577
		}
3578
3579
		// Start hidden, except for current view
3580
		if(view_et2)
3581
		{
3582
			if(hidden)
3583
			{
3584
				jQuery(_et2.DOMContainer).hide();
3585
			}
3586
		}
3587
		else
3588
		{
3589
			var app_name = _name.split('.')[0];
3590
			if(app_name && app_name != 'calendar' && egw.app(app_name))
3591
			{
3592
				// A template from another application?  Keep it up to date as state changes
3593
				this.sidebox_hooked_templates.push(_et2.widgetContainer);
3594
				// If it leaves (or reloads) remove it
3595
				jQuery(_et2.DOMContainer).one('clear',jQuery.proxy(function() {
3596
					if(app.calendar)
3597
					{
3598
						app.calendar.sidebox_hooked_templates.splice(this,1,0);
3599
					}
3600
				},this.sidebox_hooked_templates.length -1));
3601
			}
3602
		}
3603
		if(all_loaded)
3604
		{
3605
			jQuery(window).trigger('resize');
3606
			this.setState({state:this.state});
3607
3608
			// Hide loader after 1 second as a fallback, it will also be hidden
3609
			// after loading is complete.
3610
			window.setTimeout(jQuery.proxy(function() {
3611
				egw.loading_prompt(this.appname,false);
3612
			}, this),1000);
3613
3614
			// Start calendar-wide autorefresh timer to include more than just nm
3615
			this._set_autorefresh();
3616
		}
3617
	},
3618
3619
	/**
3620
	 * Set a refresh timer that works for the current view.
3621
	 * The nextmatch goes into an infinite loop if we let it autorefresh while
3622
	 * hidden.
3623
	 */
3624
	_set_autorefresh: function() {
3625
		// Listview not loaded
3626
		if(typeof app.classes.calendar.views.listview.etemplates[0] == 'string') return;
3627
3628
		var nm = app.classes.calendar.views.listview.etemplates[0].widgetContainer.getWidgetById('nm');
3629
		// nextmatch missing
3630
		if(!nm) return;
3631
3632
		var refresh_preference = "nextmatch-" + nm.options.settings.columnselection_pref + "-autorefresh";
3633
		var time = this.egw.preference(refresh_preference, 'calendar');
3634
3635
		if(this.state.view == 'listview' && time)
3636
		{
3637
			nm._set_autorefresh(time);
3638
			return;
3639
		}
3640
		else
3641
		{
3642
			window.clearInterval(nm._autorefresh_timer);
3643
		}
3644
		var self = this;
3645
		var refresh = function() {
3646
			// Deleted events are not coming properly, so clear it all
3647
			self._clear_cache();
3648
			// Force redraw to current state
3649
			self.setState({state: self.state});
3650
3651
			// This is a fast update, but misses deleted events
3652
			//app.calendar._fetch_data(app.calendar.state);
3653
		};
3654
3655
		// Start / update timer
3656
		if (this._autorefresh_timer)
3657
		{
3658
			window.clearInterval(this._autorefresh_timer);
3659
			this._autorefresh_timer = null;
3660
		}
3661
		if(time > 0)
3662
		{
3663
			this._autorefresh_timer = setInterval(jQuery.proxy(refresh, this), time * 1000);
3664
		}
3665
3666
		// Bind to tab show/hide events, so that we don't bother refreshing in the background
3667
		jQuery(nm.getInstanceManager().DOMContainer.parentNode).on('hide.calendar', jQuery.proxy(function(e) {
3668
			// Stop
3669
			window.clearInterval(this._autorefresh_timer);
3670
			jQuery(e.target).off(e);
3671
3672
			if(!time) return;
3673
3674
			// If the autorefresh time is up, bind once to trigger a refresh
3675
			// (if needed) when tab is activated again
3676
			this._autorefresh_timer = setTimeout(jQuery.proxy(function() {
3677
				// Check in case it was stopped / destroyed since
3678
				if(!this._autorefresh_timer) return;
3679
3680
				jQuery(nm.getInstanceManager().DOMContainer.parentNode).one('show.calendar',
3681
					// Important to use anonymous function instead of just 'this.refresh' because
3682
					// of the parameters passed
3683
					jQuery.proxy(function() {refresh();},this)
3684
				);
3685
			},this), time*1000);
3686
		},this));
3687
		jQuery(nm.getInstanceManager().DOMContainer.parentNode).on('show.calendar', jQuery.proxy(function(e) {
3688
			// Start normal autorefresh timer again
3689
			this._set_autorefresh(this.egw.preference(refresh_preference, 'calendar'));
3690
			jQuery(e.target).off(e);
3691
		},this));
3692
	},
3693
3694
	/**
3695
	 * Super class for the different views.
3696
	 *
3697
	 * Each separate view overrides what it needs
3698
	 */
3699
	View: {
3700
		// List of etemplates to show for this view
3701
		etemplates: ['calendar.view'],
3702
3703
		/**
3704
		 * Translated label for header
3705
		 * @param {Object} state
3706
		 * @returns {string}
3707
		 */
3708
		header: function(state) {
3709
			var formatDate = new Date(state.date);
3710
			formatDate = new Date(formatDate.valueOf() + formatDate.getTimezoneOffset() * 60 * 1000);
3711
			return app.calendar.View._owner(state) + date(egw.preference('dateformat'),formatDate);
3712
		},
3713
3714
		/**
3715
		 * If one owner, get the owner text
3716
		 *
3717
		 * @param {object} state
3718
		 */
3719
		_owner: function(state) {
3720
			var owner = '';
3721
			if(state.owner.length && state.owner.length == 1 && app.calendar.sidebox_et2)
3722
			{
3723
				var own = app.calendar.sidebox_et2.getWidgetById('owner').getDOMNode();
3724
				if(own.selectedIndex >= 0)
3725
				{
3726
					owner = own.options[own.selectedIndex].innerHTML + ": ";
3727
				}
3728
			}
3729
			return owner;
3730
		},
3731
3732
		/**
3733
		 * Get the start date for this view
3734
		 * @param {Object} state
3735
		 * @returns {Date}
3736
		 */
3737
		start_date: function(state) {
3738
			var d = state.date ? new Date(state.date) : new Date();
3739
			d.setUTCHours(0);
3740
			d.setUTCMinutes(0);
3741
			d.setUTCSeconds(0);
3742
			d.setUTCMilliseconds(0);
3743
			return d;
3744
		},
3745
		/**
3746
		 * Get the end date for this view
3747
		 * @param {Object} state
3748
		 * @returns {Date}
3749
		 */
3750
		end_date: function(state) {
3751
			var d = state.date ? new Date(state.date) : new Date();
3752
			d.setUTCHours(23);
3753
			d.setUTCMinutes(59);
3754
			d.setUTCSeconds(59);
3755
			d.setUTCMilliseconds(0);
3756
			return d;
3757
		},
3758
		/**
3759
		 * Get the owner for this view
3760
		 *
3761
		 * This is always the owner from the given state, we use a function
3762
		 * to trigger setting the widget value.
3763
		 *
3764
		 * @param {number[]|String} state state.owner List of owner IDs, or a comma seperated list
3765
		 * @returns {number[]|String}
3766
		 */
3767
		owner: function(state) {
3768
			return state.owner || 0;
3769
		},
3770
		/**
3771
		 * Should the view show the weekends
3772
		 *
3773
		 * @param {object} state
3774
		 * @returns {boolean} Current preference to show 5 or 7 days in weekview
3775
		 */
3776
		show_weekend: function(state)
3777
		{
3778
			return state.weekend;
3779
		},
3780
		/**
3781
		 * How big or small are the displayed time chunks?
3782
		 *
3783
		 * @param {object} state
3784
		 */
3785
		granularity: function(state) {
3786
			var list = egw.preference('use_time_grid','calendar');
3787
			if(list === 0 || typeof list === 'undefined')
3788
			{
3789
				return parseInt(egw.preference('interval','calendar')) || 30;
3790
			}
3791
			if(typeof list == 'string') list = list.split(',');
3792
			if(!list.indexOf && jQuery.isPlainObject(list))
3793
			{
3794
				list = jQuery.map(list, function(el) { return el; });
3795
			}
3796
			return list.indexOf(state.view) >= 0 ?
3797
				0 :
3798
				parseInt(egw.preference('interval','calendar')) || 30;
3799
		},
3800
		extend: function(sub)
3801
		{
3802
			return jQuery.extend({},this,{_super:this},sub);
3803
		},
3804
		/**
3805
		 * Determines the new date after scrolling.  The default is 1 week.
3806
		 *
3807
		 * @param {number} delta Integer for how many 'ticks' to move, positive for
3808
		 *	forward, negative for backward
3809
		 * @returns {Date}
3810
		 */
3811
		scroll: function(delta)
3812
		{
3813
			var d = new Date(app.calendar.state.date);
3814
			d.setUTCDate(d.getUTCDate() + (7 * delta));
3815
			return d;
3816
		}
3817
	},
3818
3819
	/**
3820
	 * Initialization function in order to set/unset
3821
	 * categories status.
3822
	 *
3823
	 */
3824
	category_report_init: function ()
3825
	{
3826
		var content = this.et2.getArrayMgr('content').data;
3827
		for (var i=1;i<content.grid.length;i++)
3828
		{
3829
			if (content.grid[i] != null) this.category_report_enable({id:i+'', checked:content.grid[i]['enable']});
3830
		}
3831
	},
3832
3833
	/**
3834
	 * Set/unset selected category's row
3835
	 *
3836
	 * @param {type} _widget
3837
	 * @returns {undefined}
3838
	 */
3839
	category_report_enable: function (_widget)
3840
	{
3841
		var widgets = ['[user]','[weekend]','[holidays]','[min_days]'];
3842
		var row_id = _widget.id.match(/\d+/);
3843
		var w = {};
3844
		for (var i=0;i<widgets.length;i++)
3845
		{
3846
			w = this.et2.getWidgetById(row_id+widgets[i]);
3847
			if (w) w.set_readonly(!_widget.checked);
3848
		}
3849
	},
3850
3851
	/**
3852
	 * submit function for report button
3853
	 */
3854
	category_report_submit: function ()
3855
	{
3856
		this.et2._inst.postSubmit();
3857
	},
3858
3859
	/**
3860
	 * Function to enable/disable categories
3861
	 *
3862
	 * @param {object} _widget select all checkbox
3863
	 */
3864
	category_report_selectAll: function (_widget)
3865
	{
3866
		var content = this.et2.getArrayMgr('content').data;
3867
		var checkbox = {};
3868
		var grid_index = typeof content.grid.length !='undefined'? content.grid : Object.keys(content.grid);
3869
		for (var i=1;i< grid_index.length;i++)
3870
		{
3871
			if (content.grid[i] != null)
3872
			{
3873
				checkbox = this.et2.getWidgetById(i+'[enable]');
3874
				if (checkbox)
3875
				{
3876
					checkbox.set_value(_widget.checked);
3877
					this.category_report_enable({id:checkbox.id, checked:checkbox.get_value()});
3878
				}
3879
			}
3880
		}
3881
	}
3882
});}).call(this);
3883
3884
3885
jQuery.extend(app.classes.calendar,{
3886
3887
	/**
3888
	 * This is the data cache prefix for the daywise event index cache
3889
	 * Daywise cache IDs look like: calendar_daywise::20150101 and
3890
	 * contain a list of event IDs for that day (or empty array)
3891
	 */
3892
	DAYWISE_CACHE_ID: 'calendar_daywise',
3893
3894
3895
	/**
3896
	 * Create a cache ID for the daywise cache
3897
	 *
3898
	 * @param {String|Date} date If a string, date should be in Ymd format
3899
	 * @param {String|integer|String[]} owner
3900
	 * @returns {String} Cache ID
3901
	 */
3902
	_daywise_cache_id: function(date, owner)
3903
	{
3904
		if(typeof date === 'object')
3905
		{
3906
			date =  date.getUTCFullYear() + sprintf('%02d',date.getUTCMonth()+1) + sprintf('%02d',date.getUTCDate());
3907
		}
3908
3909
	// If the owner is not set, 0, or the current user, don't bother adding it
3910
		var _owner = (owner && owner.toString() != '0') ? owner.toString() : '';
3911
		if(_owner == egw.user('account_id'))
3912
		{
3913
			_owner = '';
3914
		}
3915
		return app.classes.calendar.DAYWISE_CACHE_ID+'::'+date+(_owner ? '-' + _owner : '');
3916
	},
3917
3918
	/**
3919
	* Etemplates and settings for the different views.  Some (day view)
3920
	* use more than one template, some use the same template as others,
3921
	* most need different handling for their various attributes.
3922
	*
3923
	* Not using the standard Class.extend here because it hides the members,
3924
	* and we want to be able to look inside them.  This is done seperately instead
3925
	* of inside the normal object to allow access to the View object.
3926
	*/
3927
	views: {
3928
		day: app.classes.calendar.prototype.View.extend({
3929
			header: function(state) {
3930
				return app.calendar.View.header.call(this, state);
3931
			},
3932
			etemplates: ['calendar.view','calendar.todo'],
3933
			start_date: function(state) {
3934
				var d = app.calendar.View.start_date.call(this, state);
3935
				state.date = app.calendar.date.toString(d);
3936
				return d;
3937
			},
3938
			show_weekend: function(state) {
3939
				state.days = '1';
3940
				return true;
3941
			},
3942
			scroll: function(delta)
3943
			{
3944
				var d = new Date(app.calendar.state.date);
3945
				d.setUTCDate(d.getUTCDate() + (delta));
3946
				return d;
3947
			}
3948
		}),
3949
		day4: app.classes.calendar.prototype.View.extend({
3950
			header: function(state) {
3951
				return app.calendar.View.header.call(this, state);
3952
			},
3953
			end_date: function(state) {
3954
				var d = app.calendar.View.end_date.call(this,state);
3955
				state.days = '4';
3956
				d.setUTCHours(24*4-1);
3957
				d.setUTCMinutes(59);
3958
				d.setUTCSeconds(59);
3959
				d.setUTCMilliseconds(0);
3960
				return d;
3961
			},
3962
			show_weekend: function(state) {
3963
				state.weekend = 'true';
3964
				return true;
3965
			},
3966
			scroll: function(delta)
3967
			{
3968
				var d = new Date(app.calendar.state.date);
3969
				d.setUTCDate(d.getUTCDate() + (4 * delta));
3970
				return d;
3971
			}
3972
		}),
3973
		week: app.classes.calendar.prototype.View.extend({
3974
			header: function(state) {
3975
				var end_date = state.last;
3976
				if(!app.classes.calendar.views.week.show_weekend(state))
3977
				{
3978
					end_date = new Date(state.last);
3979
					end_date.setUTCDate(end_date.getUTCDate() - 2);
3980
				}
3981
				return app.calendar.View._owner(state) + app.calendar.egw.lang('Week') + ' ' +
3982
					app.calendar.date.week_number(state.first) + ': ' +
3983
					app.calendar.date.long_date(state.first, end_date);
3984
			},
3985
			start_date: function(state) {
3986
				return app.calendar.date.start_of_week(app.calendar.View.start_date.call(this,state));
3987
			},
3988
			end_date: function(state) {
3989
				var d = app.calendar.date.start_of_week(state.date || new Date());
3990
				// Always 7 days, we just turn weekends on or off
3991
				d.setUTCHours(24*7-1);
3992
				d.setUTCMinutes(59);
3993
				d.setUTCSeconds(59);
3994
				d.setUTCMilliseconds(0);
3995
				return d;
3996
			}
3997
		}),
3998
		weekN: app.classes.calendar.prototype.View.extend({
3999
			header: function(state) {
4000
				return  app.calendar.View._owner(state) + app.calendar.egw.lang('Week') + ' ' +
4001
					app.calendar.date.week_number(state.first) + ' - ' +
4002
					app.calendar.date.week_number(state.last) + ': ' +
4003
					app.calendar.date.long_date(state.first, state.last);
4004
			},
4005
			start_date: function(state) {
4006
				return app.calendar.date.start_of_week(app.calendar.View.start_date.call(this,state));
4007
			},
4008
			end_date: function(state) {
4009
				state.days = '' + (state.days >= 5 ? state.days : egw.preference('days_in_weekview','calendar') || 7);
4010
4011
				var d = app.calendar.date.start_of_week(app.calendar.View.start_date.call(this,state));
4012
				// Always 7 days, we just turn weekends on or off
4013
				d.setUTCHours(24*7*(parseInt(this.egw.preference('multiple_weeks','calendar')) || 3)-1);
4014
				return d;
4015
			}
4016
		}),
4017
		month: app.classes.calendar.prototype.View.extend({
4018
			header: function(state)
4019
			{
4020
				var formatDate = new Date(state.date);
4021
				formatDate = new Date(formatDate.valueOf() + formatDate.getTimezoneOffset() * 60 * 1000);
4022
				return app.calendar.View._owner(state) + app.calendar.egw.lang(date('F',formatDate)) + ' ' + date('Y',formatDate);
4023
			},
4024
			start_date: function(state) {
4025
				var d = app.calendar.View.start_date.call(this,state);
4026
				d.setUTCDate(1);
4027
				return app.calendar.date.start_of_week(d);
4028
			},
4029
			end_date: function(state) {
4030
				var d = app.calendar.View.end_date.call(this,state);
4031
				d = new Date(d.getFullYear(),d.getUTCMonth() + 1, 1,0,-d.getTimezoneOffset(),0);
4032
				d.setUTCSeconds(d.getUTCSeconds()-1);
4033
				return app.calendar.date.end_of_week(d);
4034
			},
4035
			scroll: function(delta)
4036
			{
4037
				var d = new Date(app.calendar.state.date);
4038
				// Set day to 15 so we don't get overflow on short months
4039
				// eg. Aug 31 + 1 month = Sept 31 -> Oct 1
4040
				d.setUTCDate(15);
4041
				d.setUTCMonth(d.getUTCMonth() + delta);
4042
				return d;
4043
			}
4044
		}),
4045
4046
		planner: app.classes.calendar.prototype.View.extend({
4047
			header: function(state) {
4048
				var startDate = new Date(state.first);
4049
				startDate = new Date(startDate.valueOf() + startDate.getTimezoneOffset() * 60 * 1000);
4050
4051
				var endDate = new Date(state.last);
4052
				endDate = new Date(endDate.valueOf() + endDate.getTimezoneOffset() * 60 * 1000);
4053
				return app.calendar.View._owner(state) + date(egw.preference('dateformat'),startDate) +
4054
					(startDate == endDate ? '' : ' - ' + date(egw.preference('dateformat'),endDate));
4055
			},
4056
			etemplates: ['calendar.planner'],
4057
			group_by: function(state) {
4058
				return state.sortby ? state.sortby : 0;
4059
			},
4060
			// Note: Planner uses the additional value of planner_view to determine
4061
			// the start & end dates using other view's functions
4062
			start_date: function(state) {
4063
				// Start here, in case we can't find anything better
4064
				var d = app.calendar.View.start_date.call(this, state);
4065
4066
				if(state.sortby && state.sortby === 'month')
4067
				{
4068
					d.setUTCDate(1);
4069
				}
4070
				else if (state.planner_view && app.classes.calendar.views[state.planner_view])
4071
				{
4072
					d = app.classes.calendar.views[state.planner_view].start_date.call(this,state);
4073
				}
4074
				else
4075
				{
4076
					d = app.calendar.date.start_of_week(d);
4077
					d.setUTCHours(0);
4078
					d.setUTCMinutes(0);
4079
					d.setUTCSeconds(0);
4080
					d.setUTCMilliseconds(0);
4081
					return d;
4082
				}
4083
				return d;
4084
			},
4085
			end_date: function(state) {
4086
4087
				var d = app.calendar.View.end_date.call(this, state);
4088
				if(state.sortby && state.sortby === 'month')
4089
				{
4090
					d.setUTCDate(0);
4091
					d.setUTCFullYear(d.getUTCFullYear() + 1);
4092
				}
4093
				else if (state.planner_view && app.classes.calendar.views[state.planner_view])
4094
				{
4095
					d = app.classes.calendar.views[state.planner_view].end_date.call(this,state);
4096
				}
4097
				else if (state.days)
4098
				{
4099
					// This one comes from a grid view, but we'll use it
4100
					d.setUTCDate(d.getUTCDate() + parseInt(state.days)-1);
4101
					delete state.days;
4102
				}
4103
				else
4104
				{
4105
					d = app.calendar.date.end_of_week(d);
4106
				}
4107
				return d;
4108
			},
4109
			hide_empty: function(state) {
4110
				var check = state.sortby == 'user' ? ['user','both'] : ['cat','both'];
4111
				return (check.indexOf(egw.preference('planner_show_empty_rows','calendar')) === -1);
4112
			},
4113
			scroll: function(delta)
4114
			{
4115
				if(app.calendar.state.planner_view)
4116
				{
4117
					return app.classes.calendar.views[app.calendar.state.planner_view].scroll.call(this,delta);
4118
				}
4119
				var d = new Date(app.calendar.state.date);
4120
				var days = 1;
4121
4122
				// Yearly view, grouped by month - scroll 1 month
4123
				if(app.calendar.state.sortby === 'month')
4124
				{
4125
					d.setUTCMonth(d.getUTCMonth() + delta);
4126
					d.setUTCDate(1);
4127
					d.setUTCHours(0);
4128
					d.setUTCMinutes(0);
4129
					return d;
4130
				}
4131
				// Need to set the day count, or auto date ranging takes over and
4132
				// makes things buggy
4133
				if(app.calendar.state.first && app.calendar.state.last)
4134
				{
4135
					var diff = new Date(app.calendar.state.last)  - new Date(app.calendar.state.first);
4136
					days = Math.round(diff / (1000*3600*24));
4137
				}
4138
				d.setUTCDate(d.getUTCDate() + (days*delta));
4139
				if(days > 8)
4140
				{
4141
					d = app.calendar.date.start_of_week(d);
4142
				}
4143
				return d;
4144
			}
4145
		}),
4146
4147
		listview: app.classes.calendar.prototype.View.extend({
4148
			header: function(state)
4149
			{
4150
				var startDate = new Date(state.first || state.date);
4151
				startDate = new Date(startDate.valueOf() + startDate.getTimezoneOffset() * 60 * 1000);
4152
				var start_check = ''+startDate.getFullYear() + startDate.getMonth() + startDate.getDate();
4153
4154
				var endDate = new Date(state.last || state.date);
4155
				endDate = new Date(endDate.valueOf() + endDate.getTimezoneOffset() * 60 * 1000);
4156
				var end_check = ''+endDate.getFullYear() + endDate.getMonth() + endDate.getDate();
4157
				return app.calendar.View._owner(state) +
4158
					date(egw.preference('dateformat'),startDate) +
4159
					(start_check == end_check ? '' : ' - ' + date(egw.preference('dateformat'),endDate));
4160
			},
4161
			etemplates: ['calendar.list']
4162
		})
4163
	}}
4164
);
4165